Skip to content
This repository was archived by the owner on Dec 29, 2020. It is now read-only.

Commit 3e15322

Browse files
committed
For #15
Implemented the string functions
1 parent e965981 commit 3e15322

File tree

2 files changed

+244
-1
lines changed

2 files changed

+244
-1
lines changed

Net.Http.OData.Tests/Query/QueryableExtensionsTests.cs

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,42 @@ public void Apply_Filter_Single_Property_Add_Equals_Implicit_Cast()
9595
Assert.All(results, x => Assert.Equal(39.00M, ((dynamic)x).Price));
9696
}
9797

98+
[Fact]
99+
public void Apply_Filter_Single_Property_Concat()
100+
{
101+
TestHelper.EnsureEDM();
102+
103+
var queryOptions = new ODataQueryOptions(
104+
"?$filter=concat(concat(Forename, ', '), Surname) eq 'Jess, Smith'",
105+
EntityDataModel.Current.EntitySets["Employees"],
106+
Mock.Of<IODataQueryOptionsValidator>());
107+
108+
IList<ExpandoObject> results = _employees.AsQueryable().Apply(queryOptions).ToList();
109+
110+
Assert.Equal(_employees.Where(x => (x.Forename + ", " + x.Surname) == "Jess, Smith").Count(), results.Count);
111+
112+
Assert.Single(results);
113+
114+
Assert.All(results, x => Assert.True(((dynamic)x).Forename == "Jess" && ((dynamic)x).Surname == "Smith"));
115+
}
116+
117+
[Fact]
118+
public void Apply_Filter_Single_Property_Contains()
119+
{
120+
TestHelper.EnsureEDM();
121+
122+
var queryOptions = new ODataQueryOptions(
123+
"?$filter=contains(Description, 'Case')",
124+
EntityDataModel.Current.EntitySets["Products"],
125+
Mock.Of<IODataQueryOptionsValidator>());
126+
127+
IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();
128+
129+
Assert.Equal(_products.Where(x => x.Description.Contains("Case")).Count(), results.Count);
130+
131+
Assert.All(results, x => Assert.True(((dynamic)x).Description.Contains("Case")));
132+
}
133+
98134
[Fact]
99135
public void Apply_Filter_Single_Property_Divide_Equals()
100136
{
@@ -129,6 +165,23 @@ public void Apply_Filter_Single_Property_Divide_Equals_Implicit_Cast()
129165
Assert.All(results, x => Assert.Equal(39.00M, ((dynamic)x).Price));
130166
}
131167

168+
[Fact]
169+
public void Apply_Filter_Single_Property_EndsWith()
170+
{
171+
TestHelper.EnsureEDM();
172+
173+
var queryOptions = new ODataQueryOptions(
174+
"?$filter=endswith(Description, 'Blue')",
175+
EntityDataModel.Current.EntitySets["Products"],
176+
Mock.Of<IODataQueryOptionsValidator>());
177+
178+
IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();
179+
180+
Assert.Equal(_products.Where(x => x.Description.EndsWith("Blue")).Count(), results.Count);
181+
182+
Assert.All(results, x => Assert.True(((dynamic)x).Description.EndsWith("Blue")));
183+
}
184+
132185
[Fact]
133186
public void Apply_Filter_Single_Property_Equals_Enum()
134187
{
@@ -280,6 +333,40 @@ public void Apply_Filter_Single_Property_Has_Enum()
280333
Assert.All(results, x => Assert.True(((dynamic)x).AccessLevel.HasFlag(AccessLevel.Read | AccessLevel.Write)));
281334
}
282335

336+
[Fact]
337+
public void Apply_Filter_Single_Property_IndexOf()
338+
{
339+
TestHelper.EnsureEDM();
340+
341+
var queryOptions = new ODataQueryOptions(
342+
"?$filter=indexof(Description, '64GB') eq 10",
343+
EntityDataModel.Current.EntitySets["Products"],
344+
Mock.Of<IODataQueryOptionsValidator>());
345+
346+
IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();
347+
348+
Assert.Equal(_products.Where(x => x.Description.IndexOf("64GB") == 10).Count(), results.Count);
349+
350+
Assert.All(results, x => Assert.True(((dynamic)x).Description.Contains("64GB")));
351+
}
352+
353+
[Fact]
354+
public void Apply_Filter_Single_Property_Length()
355+
{
356+
TestHelper.EnsureEDM();
357+
358+
var queryOptions = new ODataQueryOptions(
359+
"?$filter=length(Description) eq 19",
360+
EntityDataModel.Current.EntitySets["Products"],
361+
Mock.Of<IODataQueryOptionsValidator>());
362+
363+
IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();
364+
365+
Assert.Equal(_products.Where(x => x.Description.Length == 19).Count(), results.Count);
366+
367+
Assert.All(results, x => Assert.True(((dynamic)x).Description.Length == 19));
368+
}
369+
283370
[Fact]
284371
public void Apply_Filter_Single_Property_LessThan_Decimal()
285372
{
@@ -463,6 +550,40 @@ public void Apply_Filter_Single_Property_NotEquals_String()
463550
Assert.All(results, x => Assert.NotEqual("iPhone SE 64GB Blue", ((dynamic)x).Description));
464551
}
465552

553+
[Fact]
554+
public void Apply_Filter_Single_Property_StartsWith()
555+
{
556+
TestHelper.EnsureEDM();
557+
558+
var queryOptions = new ODataQueryOptions(
559+
"?$filter=startswith(Description, 'iPhone SE 64GB')",
560+
EntityDataModel.Current.EntitySets["Products"],
561+
Mock.Of<IODataQueryOptionsValidator>());
562+
563+
IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();
564+
565+
Assert.Equal(_products.Where(x => x.Description.StartsWith("iPhone SE 64GB")).Count(), results.Count);
566+
567+
Assert.All(results, x => Assert.True(((dynamic)x).Description.StartsWith("iPhone SE 64GB")));
568+
}
569+
570+
[Fact]
571+
public void Apply_Filter_Single_Property_Substring()
572+
{
573+
TestHelper.EnsureEDM();
574+
575+
var queryOptions = new ODataQueryOptions(
576+
"?$filter=substring(Description, 7) eq 'SE 64GB Blue'",
577+
EntityDataModel.Current.EntitySets["Products"],
578+
Mock.Of<IODataQueryOptionsValidator>());
579+
580+
IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();
581+
582+
Assert.Equal(_products.Where(x => x.Description.Substring(7) == "SE 64GB Blue").Count(), results.Count);
583+
584+
Assert.All(results, x => Assert.True(((dynamic)x).Description == "iPhone SE 64GB Blue"));
585+
}
586+
466587
[Fact]
467588
public void Apply_Filter_Single_Property_Subtract_Equals()
468589
{
@@ -497,6 +618,63 @@ public void Apply_Filter_Single_Property_Subtract_Equals_Implicit_Cast()
497618
Assert.All(results, x => Assert.Equal(39M, ((dynamic)x).Price));
498619
}
499620

621+
[Fact]
622+
public void Apply_Filter_Single_Property_ToLower()
623+
{
624+
TestHelper.EnsureEDM();
625+
626+
var queryOptions = new ODataQueryOptions(
627+
"?$filter=tolower(Forename) eq 'jess'",
628+
EntityDataModel.Current.EntitySets["Employees"],
629+
Mock.Of<IODataQueryOptionsValidator>());
630+
631+
IList<ExpandoObject> results = _employees.AsQueryable().Apply(queryOptions).ToList();
632+
633+
Assert.Equal(_employees.Where(x => x.Forename.ToLower() == "jess").Count(), results.Count);
634+
635+
Assert.Single(results);
636+
637+
Assert.All(results, x => Assert.True(((dynamic)x).Forename == "Jess"));
638+
}
639+
640+
[Fact]
641+
public void Apply_Filter_Single_Property_ToUpper()
642+
{
643+
TestHelper.EnsureEDM();
644+
645+
var queryOptions = new ODataQueryOptions(
646+
"?$filter=toupper(Forename) eq 'JESS'",
647+
EntityDataModel.Current.EntitySets["Employees"],
648+
Mock.Of<IODataQueryOptionsValidator>());
649+
650+
IList<ExpandoObject> results = _employees.AsQueryable().Apply(queryOptions).ToList();
651+
652+
Assert.Equal(_employees.Where(x => x.Forename.ToUpper() == "JESS").Count(), results.Count);
653+
654+
Assert.Single(results);
655+
656+
Assert.All(results, x => Assert.True(((dynamic)x).Forename == "Jess"));
657+
}
658+
659+
[Fact]
660+
public void Apply_Filter_Single_Property_Trim()
661+
{
662+
TestHelper.EnsureEDM();
663+
664+
var queryOptions = new ODataQueryOptions(
665+
"?$filter=trim(Forename) eq 'Jess'",
666+
EntityDataModel.Current.EntitySets["Employees"],
667+
Mock.Of<IODataQueryOptionsValidator>());
668+
669+
IList<ExpandoObject> results = _employees.AsQueryable().Apply(queryOptions).ToList();
670+
671+
Assert.Equal(_employees.Where(x => x.Forename.Trim() == "Jess").Count(), results.Count);
672+
673+
Assert.Single(results);
674+
675+
Assert.All(results, x => Assert.True(((dynamic)x).Forename == "Jess"));
676+
}
677+
500678
[Fact]
501679
public void Apply_Filter_Single_PropertyPath_Equals_String()
502680
{

Net.Http.OData/Query/Linq/FilterBinder.cs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,35 @@
1212
using System;
1313
using System.Linq;
1414
using System.Linq.Expressions;
15+
using System.Reflection;
1516
using Net.Http.OData.Query.Expressions;
1617

1718
namespace Net.Http.OData.Query.Linq
1819
{
1920
internal static class FilterBinder
2021
{
22+
private static readonly MethodInfo s_enumHasFlag = typeof(Enum).GetMethod("HasFlag");
23+
24+
private static readonly MethodInfo s_stringConcat = typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string) });
25+
26+
private static readonly MethodInfo s_stringContains = typeof(string).GetMethod("Contains", new[] { typeof(string) });
27+
28+
private static readonly MethodInfo s_stringEndsWith = typeof(string).GetMethod("EndsWith", new[] { typeof(string) });
29+
30+
private static readonly MethodInfo s_stringIndexOf = typeof(string).GetMethod("IndexOf", new[] { typeof(string) });
31+
32+
private static readonly PropertyInfo s_stringLength = typeof(string).GetProperty("Length");
33+
34+
private static readonly MethodInfo s_stringStartsWith = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
35+
36+
private static readonly MethodInfo s_stringSubstring = typeof(string).GetMethod("Substring", new[] { typeof(int) });
37+
38+
private static readonly MethodInfo s_stringToLower = typeof(string).GetMethod("ToLower", Type.EmptyTypes);
39+
40+
private static readonly MethodInfo s_stringToUpper = typeof(string).GetMethod("ToUpper", Type.EmptyTypes);
41+
42+
private static readonly MethodInfo s_stringTrim = typeof(string).GetMethod("Trim", Type.EmptyTypes);
43+
2144
internal static IQueryable ApplyFilter(this IQueryable queryable, ODataQueryOptions queryOptions)
2245
{
2346
if (queryOptions.Filter == null)
@@ -52,6 +75,9 @@ private static Expression Bind(QueryNode queryNode)
5275
case QueryNodeKind.Constant:
5376
return Bind((ConstantNode)queryNode);
5477

78+
case QueryNodeKind.FunctionCall:
79+
return Bind((FunctionCallNode)queryNode);
80+
5581
case QueryNodeKind.PropertyAccess:
5682
return Bind((PropertyAccessNode)queryNode);
5783

@@ -93,7 +119,7 @@ private static Expression Bind(BinaryOperatorNode binaryOperatorNode)
93119
return Expression.Equal(leftExpression, rightExpression);
94120

95121
case BinaryOperatorKind.Has:
96-
return Expression.Call(leftExpression, typeof(Enum).GetMethod("HasFlag"), Expression.Convert(rightExpression, typeof(Enum)));
122+
return Expression.Call(leftExpression, s_enumHasFlag, Expression.Convert(rightExpression, typeof(Enum)));
97123

98124
case BinaryOperatorKind.GreaterThan:
99125
return Expression.GreaterThan(leftExpression, rightExpression);
@@ -145,6 +171,45 @@ private static Expression Bind(BinaryOperatorNode binaryOperatorNode)
145171
private static Expression Bind(ConstantNode constantNode)
146172
=> constantNode.Value == null ? Expression.Constant(null) : Expression.Constant(constantNode.Value, constantNode.EdmType.ClrType);
147173

174+
private static Expression Bind(FunctionCallNode functionCallNode)
175+
{
176+
switch (functionCallNode.Name)
177+
{
178+
case "concat":
179+
return Expression.Call(s_stringConcat, Bind(functionCallNode.Parameters[0]), Bind(functionCallNode.Parameters[1]));
180+
181+
case "contains":
182+
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringContains, Bind(functionCallNode.Parameters[1]));
183+
184+
case "endswith":
185+
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringEndsWith, Bind(functionCallNode.Parameters[1]));
186+
187+
case "indexof":
188+
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringIndexOf, Bind(functionCallNode.Parameters[1]));
189+
190+
case "length":
191+
return Expression.Property(Bind(functionCallNode.Parameters[0]), s_stringLength);
192+
193+
case "startswith":
194+
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringStartsWith, Bind(functionCallNode.Parameters[1]));
195+
196+
case "substring":
197+
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringSubstring, Bind(functionCallNode.Parameters[1]));
198+
199+
case "tolower":
200+
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringToLower);
201+
202+
case "toupper":
203+
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringToUpper);
204+
205+
case "trim":
206+
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringTrim);
207+
208+
default:
209+
throw new NotSupportedException($"The function '{functionCallNode.Name}' is not supported by this service.");
210+
}
211+
}
212+
148213
private static Expression Bind(PropertyAccessNode propertyAccessNode)
149214
=> propertyAccessNode.PropertyPath.MemberExpression;
150215

0 commit comments

Comments
 (0)