Skip to content

Commit 786db81

Browse files
author
Ben Robinson
committed
Added ArrayContains to CosmosLinqExtensions to allow partial matching
The `Array_Contains` funtion CosmosDB Sql has a 3rd parameter which allows it to do a partial match on the given item. This is unable to be called with the built in Linq `array.Contains(item)` extension methods. This adds this adds an explicit mapping to this function to allow it to be called in Linq like this: `documents.Where(document => document.ObjectArray.ArrayContains(new { Name = "abc" }, true))`
1 parent 0958198 commit 786db81

File tree

7 files changed

+194
-2
lines changed

7 files changed

+194
-2
lines changed

Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs

+28-1
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,13 @@ public ArrayContainsVisitor()
4242
{
4343
}
4444

45+
public bool UsePartialMatchParameter { get; set; }
46+
4547
protected override SqlScalarExpression VisitImplicit(MethodCallExpression methodCallExpression, TranslationContext context)
4648
{
4749
Expression searchList = null;
4850
Expression searchExpression = null;
51+
Expression partialMatchExpression = null;
4952

5053
// If non static Contains
5154
if (methodCallExpression.Arguments.Count == 1)
@@ -59,6 +62,13 @@ protected override SqlScalarExpression VisitImplicit(MethodCallExpression method
5962
searchList = methodCallExpression.Arguments[0];
6063
searchExpression = methodCallExpression.Arguments[1];
6164
}
65+
// if CosmosLinqExtensions.ArrayContains extension method which includes partial match parameter
66+
else if (this.UsePartialMatchParameter && methodCallExpression.Arguments.Count == 3)
67+
{
68+
searchList = methodCallExpression.Arguments[0];
69+
searchExpression = methodCallExpression.Arguments[1];
70+
partialMatchExpression = methodCallExpression.Arguments[2];
71+
}
6272

6373
if (searchList == null || searchExpression == null)
6474
{
@@ -72,7 +82,20 @@ protected override SqlScalarExpression VisitImplicit(MethodCallExpression method
7282

7383
SqlScalarExpression array = ExpressionToSql.VisitScalarExpression(searchList, context);
7484
SqlScalarExpression expression = ExpressionToSql.VisitScalarExpression(searchExpression, context);
75-
return SqlFunctionCallScalarExpression.CreateBuiltin("ARRAY_CONTAINS", array, expression);
85+
86+
SqlScalarExpression[] arrayContainsArgs;
87+
88+
if (partialMatchExpression is null)
89+
{
90+
arrayContainsArgs = new[] { array, expression };
91+
}
92+
else
93+
{
94+
SqlScalarExpression partialMatch = ExpressionToSql.VisitScalarExpression(partialMatchExpression, context);
95+
arrayContainsArgs = new[] { array, expression, partialMatch };
96+
}
97+
98+
return SqlFunctionCallScalarExpression.CreateBuiltin("ARRAY_CONTAINS", arrayContainsArgs);
7699
}
77100

78101
private SqlScalarExpression VisitIN(Expression expression, ConstantExpression constantExpressionList, TranslationContext context)
@@ -177,6 +200,10 @@ static ArrayBuiltinFunctions()
177200
{
178201
"ToList",
179202
new ArrayToArrayVisitor()
203+
},
204+
{
205+
nameof(CosmosLinqExtensions.ArrayContains),
206+
new ArrayContainsVisitor() { UsePartialMatchParameter = true }
180207
}
181208
};
182209
}

Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/BuiltinFunctionVisitor.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ public static SqlScalarExpression VisitBuiltinFunctionCall(MethodCallExpression
5959
if (methodCallExpression.Method.Name == nameof(CosmosLinqExtensions.DocumentId))
6060
{
6161
return OtherBuiltinSystemFunctions.Visit(methodCallExpression, context);
62-
}
62+
}
63+
64+
if (methodCallExpression.Method.Name == nameof(CosmosLinqExtensions.ArrayContains))
65+
{
66+
return ArrayBuiltinFunctions.Visit(methodCallExpression, context);
67+
}
6368

6469
return TypeCheckFunctions.Visit(methodCallExpression, context);
6570
}

Microsoft.Azure.Cosmos/src/Linq/CosmosLinqExtensions.cs

+30
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Microsoft.Azure.Cosmos.Linq
66
{
77
using System;
8+
using System.Collections;
89
using System.Collections.Generic;
910
using System.Diagnostics;
1011
using System.Linq;
@@ -237,6 +238,35 @@ public static bool RegexMatch(this object obj, string regularExpression, string
237238
throw new NotImplementedException(ClientResources.TypeCheckExtensionFunctionsNotImplemented);
238239
}
239240

241+
/// <summary>
242+
/// Returns a boolean indicating whether the array contains the specified value.
243+
/// You can check for a partial or full match of an object by using a boolean expression within the function.
244+
/// For more information, see https://learn.microsoft.com/en-gb/azure/cosmos-db/nosql/query/array-contains.
245+
/// This method is to be used in LINQ expressions only and will be evaluated on server.
246+
/// There's no implementation provided in the client library.
247+
/// </summary>
248+
/// <param name="obj"></param>
249+
/// <param name="itemToMatch">The value to search within the array.</param>
250+
/// <param name="partialMatch">Indicating whether the search should check for a partial match (true) or a full match (false).</param>
251+
/// <returns>Returns true if the array array contains the specified value; otherwise, false.</returns>
252+
/// <example>
253+
/// <code>
254+
/// <![CDATA[
255+
/// var matched = documents.Where(document => document.Namess.ArrayContains(<itemToMatch>, <partialMatch>));
256+
/// // To do a partial match on an array of objects, pass in an anonymous object set partialMatch to true
257+
/// var matched = documents.Where(document => document.ObjectArray.ArrayContains(new { Name = <name> }, true));
258+
/// ]]>
259+
/// </code>
260+
/// </example>
261+
public static bool ArrayContains(this IEnumerable obj, object itemToMatch, bool partialMatch)
262+
{
263+
// The signature for this is not generic so the user can pass in anonymous type for the item to match
264+
// e.g documents.Where(document => document.FooItems.ArrayContains(new { Name = "Bar" }, true)
265+
// partialMatch could have a default values (bool partialMatch = false) but those are not valid in expressions
266+
// (see error CS0854) and this method will only be used in expressions, so not point adding it
267+
throw new NotImplementedException(ClientResources.TypeCheckExtensionFunctionsNotImplemented);
268+
}
269+
240270
/// <summary>
241271
/// This method generate query definition from LINQ query.
242272
/// </summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<Results>
2+
<Result>
3+
<Input>
4+
<Description><![CDATA[ArrayContains in Select clause with int value and match partial true]]></Description>
5+
<Expression><![CDATA[query.Select(doc => doc.ArrayField.ArrayContains(Convert(1, Object), True))]]></Expression>
6+
</Input>
7+
<Output>
8+
<SqlQuery><![CDATA[
9+
SELECT VALUE ARRAY_CONTAINS(root["ArrayField"], 1, true)
10+
FROM root]]></SqlQuery>
11+
</Output>
12+
</Result>
13+
<Result>
14+
<Input>
15+
<Description><![CDATA[ArrayContains in Filter clause with int value and match partial true]]></Description>
16+
<Expression><![CDATA[query.Where(doc => doc.ArrayField.ArrayContains(Convert(1, Object), True))]]></Expression>
17+
</Input>
18+
<Output>
19+
<SqlQuery><![CDATA[
20+
SELECT VALUE root
21+
FROM root
22+
WHERE ARRAY_CONTAINS(root["ArrayField"], 1, true)]]></SqlQuery>
23+
</Output>
24+
</Result>
25+
<Result>
26+
<Input>
27+
<Description><![CDATA[ArrayContains in Select clause with object value and match partial true]]></Description>
28+
<Expression><![CDATA[query.Select(doc => doc.ObjectArrayField.ArrayContains(new AnonymousType(Field = "abc"), True))]]></Expression>
29+
</Input>
30+
<Output>
31+
<SqlQuery><![CDATA[
32+
SELECT VALUE ARRAY_CONTAINS(root["ObjectArrayField"], {"Field": "abc"}, true)
33+
FROM root]]></SqlQuery>
34+
</Output>
35+
</Result>
36+
<Result>
37+
<Input>
38+
<Description><![CDATA[ArrayContains in Filter clause with object value and match partial true]]></Description>
39+
<Expression><![CDATA[query.Where(doc => doc.ObjectArrayField.ArrayContains(new AnonymousType(Field = "abc"), True))]]></Expression>
40+
</Input>
41+
<Output>
42+
<SqlQuery><![CDATA[
43+
SELECT VALUE root
44+
FROM root
45+
WHERE ARRAY_CONTAINS(root["ObjectArrayField"], {"Field": "abc"}, true)]]></SqlQuery>
46+
</Output>
47+
</Result>
48+
<Result>
49+
<Input>
50+
<Description><![CDATA[ArrayContains in Select clause with int value and match partial false]]></Description>
51+
<Expression><![CDATA[query.Select(doc => doc.ArrayField.ArrayContains(Convert(1, Object), False))]]></Expression>
52+
</Input>
53+
<Output>
54+
<SqlQuery><![CDATA[
55+
SELECT VALUE ARRAY_CONTAINS(root["ArrayField"], 1, false)
56+
FROM root]]></SqlQuery>
57+
</Output>
58+
</Result>
59+
<Result>
60+
<Input>
61+
<Description><![CDATA[ArrayContains in Filter clause with int value and match partial false]]></Description>
62+
<Expression><![CDATA[query.Where(doc => doc.ArrayField.ArrayContains(Convert(1, Object), False))]]></Expression>
63+
</Input>
64+
<Output>
65+
<SqlQuery><![CDATA[
66+
SELECT VALUE root
67+
FROM root
68+
WHERE ARRAY_CONTAINS(root["ArrayField"], 1, false)]]></SqlQuery>
69+
</Output>
70+
</Result>
71+
<Result>
72+
<Input>
73+
<Description><![CDATA[ArrayContains in Select clause with object value and match partial false]]></Description>
74+
<Expression><![CDATA[query.Select(doc => doc.ObjectArrayField.ArrayContains(new AnonymousType(Field = "abc"), False))]]></Expression>
75+
</Input>
76+
<Output>
77+
<SqlQuery><![CDATA[
78+
SELECT VALUE ARRAY_CONTAINS(root["ObjectArrayField"], {"Field": "abc"}, false)
79+
FROM root]]></SqlQuery>
80+
</Output>
81+
</Result>
82+
<Result>
83+
<Input>
84+
<Description><![CDATA[ArrayContains in Filter clause with object value and match partial false]]></Description>
85+
<Expression><![CDATA[query.Where(doc => doc.ObjectArrayField.ArrayContains(new AnonymousType(Field = "abc"), False))]]></Expression>
86+
</Input>
87+
<Output>
88+
<SqlQuery><![CDATA[
89+
SELECT VALUE root
90+
FROM root
91+
WHERE ARRAY_CONTAINS(root["ObjectArrayField"], {"Field": "abc"}, false)]]></SqlQuery>
92+
</Output>
93+
</Result>
94+
</Results>

Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTranslationBaselineTests.cs

+26
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ internal class DataObject : LinqTestObject
117117
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value false
118118
public bool BooleanField;
119119
public SimpleObject ObjectField = new SimpleObject();
120+
public SimpleObject[] ObjectArrayField = new SimpleObject[0];
120121
public Guid GuidField;
121122
#pragma warning restore // Field is never assigned to, and will always have its default value false
122123

@@ -346,6 +347,31 @@ public void TestRegexMatchFunction()
346347
this.ExecuteTestSuite(inputs);
347348
}
348349

350+
[TestMethod]
351+
public void TestArrayContainsBuiltinFunction()
352+
{
353+
// Similar to the type checking function, Array_Contains are not supported client side.
354+
// Therefore these methods are verified with baseline only.
355+
List<DataObject> data = new List<DataObject>();
356+
IOrderedQueryable<DataObject> query = testContainer.GetItemLinqQueryable<DataObject>(allowSynchronousQueryExecution: true);
357+
Func<bool, IQueryable<DataObject>> getQuery = useQuery => useQuery ? query : data.AsQueryable();
358+
359+
List<LinqTestInput> inputs = new List<LinqTestInput>
360+
{
361+
new LinqTestInput("ArrayContains in Select clause with int value and match partial true", b => getQuery(b).Select(doc => doc.ArrayField.ArrayContains(1, true))),
362+
new LinqTestInput("ArrayContains in Filter clause with int value and match partial true", b => getQuery(b).Where(doc => doc.ArrayField.ArrayContains(1, true))),
363+
new LinqTestInput("ArrayContains in Select clause with object value and match partial true", b => getQuery(b).Select(doc => doc.ObjectArrayField.ArrayContains(new { Field = "abc" }, true))),
364+
new LinqTestInput("ArrayContains in Filter clause with object value and match partial true", b => getQuery(b).Where(doc => doc.ObjectArrayField.ArrayContains(new { Field = "abc" }, true))),
365+
366+
new LinqTestInput("ArrayContains in Select clause with int value and match partial false", b => getQuery(b).Select(doc => doc.ArrayField.ArrayContains(1, false))),
367+
new LinqTestInput("ArrayContains in Filter clause with int value and match partial false", b => getQuery(b).Where(doc => doc.ArrayField.ArrayContains(1, false))),
368+
new LinqTestInput("ArrayContains in Select clause with object value and match partial false", b => getQuery(b).Select(doc => doc.ObjectArrayField.ArrayContains(new { Field = "abc" }, false))),
369+
new LinqTestInput("ArrayContains in Filter clause with object value and match partial false", b => getQuery(b).Where(doc => doc.ObjectArrayField.ArrayContains(new { Field = "abc" }, false))),
370+
};
371+
372+
this.ExecuteTestSuite(inputs);
373+
}
374+
349375
[TestMethod]
350376
public void TestMemberInitializer()
351377
{

Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@
112112
<Content Include="BaselineTest\TestBaseline\IndexMetricsParserBaselineTest.IndexUtilizationHeaderLengthTest.xml">
113113
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
114114
</Content>
115+
<Content Include="BaselineTest\TestBaseline\LinqTranslationBaselineTests.TestArrayContainsBuiltinFunction.xml">
116+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
117+
</Content>
115118
<Content Include="BaselineTest\TestBaseline\QueryAdvisorBaselineTest.QueryAdviceParse.xml">
116119
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
117120
</Content>

Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json

+7
Original file line numberDiff line numberDiff line change
@@ -6680,6 +6680,13 @@
66806680
"Microsoft.Azure.Cosmos.Linq.CosmosLinqExtensions;System.Object;IsAbstract:True;IsSealed:True;IsInterface:False;IsEnum:False;IsClass:True;IsValueType:False;IsNested:False;IsGenericType:False;IsSerializable:False": {
66816681
"Subclasses": {},
66826682
"Members": {
6683+
"Boolean ArrayContains(System.Collections.IEnumerable, System.Object, Boolean)[System.Runtime.CompilerServices.ExtensionAttribute()]": {
6684+
"Type": "Method",
6685+
"Attributes": [
6686+
"ExtensionAttribute"
6687+
],
6688+
"MethodInfo": "Boolean ArrayContains(System.Collections.IEnumerable, System.Object, Boolean);IsAbstract:False;IsStatic:True;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;"
6689+
},
66836690
"Boolean IsArray(System.Object)[System.Runtime.CompilerServices.ExtensionAttribute()]": {
66846691
"Type": "Method",
66856692
"Attributes": [

0 commit comments

Comments
 (0)