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

Commit

Permalink
For #15
Browse files Browse the repository at this point in the history
Implemented the string functions
  • Loading branch information
TrevorPilley committed Nov 13, 2020
1 parent e965981 commit 3e15322
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 1 deletion.
178 changes: 178 additions & 0 deletions Net.Http.OData.Tests/Query/QueryableExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,42 @@ public void Apply_Filter_Single_Property_Add_Equals_Implicit_Cast()
Assert.All(results, x => Assert.Equal(39.00M, ((dynamic)x).Price));
}

[Fact]
public void Apply_Filter_Single_Property_Concat()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$filter=concat(concat(Forename, ', '), Surname) eq 'Jess, Smith'",
EntityDataModel.Current.EntitySets["Employees"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = _employees.AsQueryable().Apply(queryOptions).ToList();

Assert.Equal(_employees.Where(x => (x.Forename + ", " + x.Surname) == "Jess, Smith").Count(), results.Count);

Assert.Single(results);

Assert.All(results, x => Assert.True(((dynamic)x).Forename == "Jess" && ((dynamic)x).Surname == "Smith"));
}

[Fact]
public void Apply_Filter_Single_Property_Contains()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$filter=contains(Description, 'Case')",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();

Assert.Equal(_products.Where(x => x.Description.Contains("Case")).Count(), results.Count);

Assert.All(results, x => Assert.True(((dynamic)x).Description.Contains("Case")));
}

[Fact]
public void Apply_Filter_Single_Property_Divide_Equals()
{
Expand Down Expand Up @@ -129,6 +165,23 @@ public void Apply_Filter_Single_Property_Divide_Equals_Implicit_Cast()
Assert.All(results, x => Assert.Equal(39.00M, ((dynamic)x).Price));
}

[Fact]
public void Apply_Filter_Single_Property_EndsWith()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$filter=endswith(Description, 'Blue')",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();

Assert.Equal(_products.Where(x => x.Description.EndsWith("Blue")).Count(), results.Count);

Assert.All(results, x => Assert.True(((dynamic)x).Description.EndsWith("Blue")));
}

[Fact]
public void Apply_Filter_Single_Property_Equals_Enum()
{
Expand Down Expand Up @@ -280,6 +333,40 @@ public void Apply_Filter_Single_Property_Has_Enum()
Assert.All(results, x => Assert.True(((dynamic)x).AccessLevel.HasFlag(AccessLevel.Read | AccessLevel.Write)));
}

[Fact]
public void Apply_Filter_Single_Property_IndexOf()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$filter=indexof(Description, '64GB') eq 10",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();

Assert.Equal(_products.Where(x => x.Description.IndexOf("64GB") == 10).Count(), results.Count);

Assert.All(results, x => Assert.True(((dynamic)x).Description.Contains("64GB")));
}

[Fact]
public void Apply_Filter_Single_Property_Length()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$filter=length(Description) eq 19",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();

Assert.Equal(_products.Where(x => x.Description.Length == 19).Count(), results.Count);

Assert.All(results, x => Assert.True(((dynamic)x).Description.Length == 19));
}

[Fact]
public void Apply_Filter_Single_Property_LessThan_Decimal()
{
Expand Down Expand Up @@ -463,6 +550,40 @@ public void Apply_Filter_Single_Property_NotEquals_String()
Assert.All(results, x => Assert.NotEqual("iPhone SE 64GB Blue", ((dynamic)x).Description));
}

[Fact]
public void Apply_Filter_Single_Property_StartsWith()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$filter=startswith(Description, 'iPhone SE 64GB')",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();

Assert.Equal(_products.Where(x => x.Description.StartsWith("iPhone SE 64GB")).Count(), results.Count);

Assert.All(results, x => Assert.True(((dynamic)x).Description.StartsWith("iPhone SE 64GB")));
}

[Fact]
public void Apply_Filter_Single_Property_Substring()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$filter=substring(Description, 7) eq 'SE 64GB Blue'",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = _products.AsQueryable().Apply(queryOptions).ToList();

Assert.Equal(_products.Where(x => x.Description.Substring(7) == "SE 64GB Blue").Count(), results.Count);

Assert.All(results, x => Assert.True(((dynamic)x).Description == "iPhone SE 64GB Blue"));
}

[Fact]
public void Apply_Filter_Single_Property_Subtract_Equals()
{
Expand Down Expand Up @@ -497,6 +618,63 @@ public void Apply_Filter_Single_Property_Subtract_Equals_Implicit_Cast()
Assert.All(results, x => Assert.Equal(39M, ((dynamic)x).Price));
}

[Fact]
public void Apply_Filter_Single_Property_ToLower()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$filter=tolower(Forename) eq 'jess'",
EntityDataModel.Current.EntitySets["Employees"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = _employees.AsQueryable().Apply(queryOptions).ToList();

Assert.Equal(_employees.Where(x => x.Forename.ToLower() == "jess").Count(), results.Count);

Assert.Single(results);

Assert.All(results, x => Assert.True(((dynamic)x).Forename == "Jess"));
}

[Fact]
public void Apply_Filter_Single_Property_ToUpper()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$filter=toupper(Forename) eq 'JESS'",
EntityDataModel.Current.EntitySets["Employees"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = _employees.AsQueryable().Apply(queryOptions).ToList();

Assert.Equal(_employees.Where(x => x.Forename.ToUpper() == "JESS").Count(), results.Count);

Assert.Single(results);

Assert.All(results, x => Assert.True(((dynamic)x).Forename == "Jess"));
}

[Fact]
public void Apply_Filter_Single_Property_Trim()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$filter=trim(Forename) eq 'Jess'",
EntityDataModel.Current.EntitySets["Employees"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = _employees.AsQueryable().Apply(queryOptions).ToList();

Assert.Equal(_employees.Where(x => x.Forename.Trim() == "Jess").Count(), results.Count);

Assert.Single(results);

Assert.All(results, x => Assert.True(((dynamic)x).Forename == "Jess"));
}

[Fact]
public void Apply_Filter_Single_PropertyPath_Equals_String()
{
Expand Down
67 changes: 66 additions & 1 deletion Net.Http.OData/Query/Linq/FilterBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,35 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Net.Http.OData.Query.Expressions;

namespace Net.Http.OData.Query.Linq
{
internal static class FilterBinder
{
private static readonly MethodInfo s_enumHasFlag = typeof(Enum).GetMethod("HasFlag");

private static readonly MethodInfo s_stringConcat = typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string) });

private static readonly MethodInfo s_stringContains = typeof(string).GetMethod("Contains", new[] { typeof(string) });

private static readonly MethodInfo s_stringEndsWith = typeof(string).GetMethod("EndsWith", new[] { typeof(string) });

private static readonly MethodInfo s_stringIndexOf = typeof(string).GetMethod("IndexOf", new[] { typeof(string) });

private static readonly PropertyInfo s_stringLength = typeof(string).GetProperty("Length");

private static readonly MethodInfo s_stringStartsWith = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });

private static readonly MethodInfo s_stringSubstring = typeof(string).GetMethod("Substring", new[] { typeof(int) });

private static readonly MethodInfo s_stringToLower = typeof(string).GetMethod("ToLower", Type.EmptyTypes);

private static readonly MethodInfo s_stringToUpper = typeof(string).GetMethod("ToUpper", Type.EmptyTypes);

private static readonly MethodInfo s_stringTrim = typeof(string).GetMethod("Trim", Type.EmptyTypes);

internal static IQueryable ApplyFilter(this IQueryable queryable, ODataQueryOptions queryOptions)
{
if (queryOptions.Filter == null)
Expand Down Expand Up @@ -52,6 +75,9 @@ private static Expression Bind(QueryNode queryNode)
case QueryNodeKind.Constant:
return Bind((ConstantNode)queryNode);

case QueryNodeKind.FunctionCall:
return Bind((FunctionCallNode)queryNode);

case QueryNodeKind.PropertyAccess:
return Bind((PropertyAccessNode)queryNode);

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

case BinaryOperatorKind.Has:
return Expression.Call(leftExpression, typeof(Enum).GetMethod("HasFlag"), Expression.Convert(rightExpression, typeof(Enum)));
return Expression.Call(leftExpression, s_enumHasFlag, Expression.Convert(rightExpression, typeof(Enum)));

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

private static Expression Bind(FunctionCallNode functionCallNode)
{
switch (functionCallNode.Name)
{
case "concat":
return Expression.Call(s_stringConcat, Bind(functionCallNode.Parameters[0]), Bind(functionCallNode.Parameters[1]));

case "contains":
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringContains, Bind(functionCallNode.Parameters[1]));

case "endswith":
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringEndsWith, Bind(functionCallNode.Parameters[1]));

case "indexof":
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringIndexOf, Bind(functionCallNode.Parameters[1]));

case "length":
return Expression.Property(Bind(functionCallNode.Parameters[0]), s_stringLength);

case "startswith":
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringStartsWith, Bind(functionCallNode.Parameters[1]));

case "substring":
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringSubstring, Bind(functionCallNode.Parameters[1]));

case "tolower":
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringToLower);

case "toupper":
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringToUpper);

case "trim":
return Expression.Call(Bind(functionCallNode.Parameters[0]), s_stringTrim);

default:
throw new NotSupportedException($"The function '{functionCallNode.Name}' is not supported by this service.");
}
}

private static Expression Bind(PropertyAccessNode propertyAccessNode)
=> propertyAccessNode.PropertyPath.MemberExpression;

Expand Down

0 comments on commit 3e15322

Please sign in to comment.