Skip to content

Commit

Permalink
Merge branch 'master' into pr/3
Browse files Browse the repository at this point in the history
  • Loading branch information
zHaytam committed Nov 14, 2021
2 parents f305d15 + 6e6f28e commit 4ce2dfe
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
*.userosscache
*.sln.docstates

.vscode/**/*
nuget.config
.nuspec
Nuget.csproj

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

Expand Down
12 changes: 6 additions & 6 deletions DynamicExpressions.UnitTests/Data.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
namespace DynamicExpressions.UnitTests
{
internal class Entry
internal class Entry<T>
{
public Entry(int id, SubEntry subEntry = null)
public Entry(int id, SubEntry<T> subEntry = null)
{
Id = id;
SubEntry = subEntry;
}

public int Id { get; }
public SubEntry SubEntry { get; }
public SubEntry<T> SubEntry { get; }
}

internal class SubEntry
internal class SubEntry<T>
{
public SubEntry(string title)
public SubEntry(T title)
{
Title = title;
}

public string Title { get; }
public T Title { get; }
}
}
87 changes: 76 additions & 11 deletions DynamicExpressions.UnitTests/PredicateTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using Xunit;
using Xunit.Extensions;

namespace DynamicExpressions.UnitTests
{
Expand All @@ -21,31 +18,99 @@ public class PredicateTests
public void GetPredicate_ShouldHandleNumericalOperators(int id, string title,
string property, FilterOperator op, object value)
{
var entry = new Entry(id, new SubEntry(title));
var predicate = DynamicExpressions.GetPredicate<Entry>(property, op, value).Compile();
var entry = new Entry<string>(id, new SubEntry<string>(title));
var predicate = DynamicExpressions.GetPredicate<Entry<string>>(property, op, value).Compile();
Assert.True(predicate(entry));
}

[Theory]
[InlineData(1, true, "SubEntry.Title", FilterOperator.Equals, true)]
[InlineData(1, true, "SubEntry.Title", FilterOperator.Equals, "true")]
[InlineData(1, true, "SubEntry.Title", FilterOperator.Equals, "True")]
[InlineData(1, false, "SubEntry.Title", FilterOperator.Equals, "False")]
[InlineData(1, false, "SubEntry.Title", FilterOperator.Equals, "false")]
[InlineData(1, false, "SubEntry.Title", FilterOperator.Equals, false)]
public void GetPredicate_ShouldHandleEqualsGenericOperators<T>(int id, T title,
string property, FilterOperator op, object value)
{
var entry = new Entry<T>(id, new SubEntry<T>(title));
var predicate = DynamicExpressions.GetPredicate<Entry<T>>(property, op, value).Compile();
Assert.True(predicate(entry));
}

[Theory]
[InlineData(3, "Title 3", FilterOperator.Contains, "3")]
[InlineData(3, "Title 3", FilterOperator.NotContains, "5")]
[InlineData(4, "Title 4", FilterOperator.StartsWith, "Title")]
[InlineData(5, "Title 5", FilterOperator.EndsWith, "5")]
public void GetPredicate_ShouldHandleNestedStringOperators(int id, string title, FilterOperator op, object value)
{
var entry = new Entry(id, new SubEntry(title));
var predicate = DynamicExpressions.GetPredicate<Entry>("SubEntry.Title", op, value).Compile();
var entry = new Entry<string>(id, new SubEntry<string>(title));
var predicate = DynamicExpressions.GetPredicate<Entry<string>>("SubEntry.Title", op, value).Compile();
Assert.True(predicate(entry));
}

[Theory]
[InlineData(3, "", FilterOperator.IsEmpty, "doesn't matter. ignored")]
[InlineData(3, null, FilterOperator.IsEmpty, "")]
[InlineData(3, "Title 3", FilterOperator.IsNotEmpty, "")]
public void GetPredicate_ShouldHandleNullOrEmtpyStringOperators(int id, string title, FilterOperator op, object value)
{
var entry = new Entry<string>(id, new SubEntry<string>(title));
var predicate = DynamicExpressions.GetPredicate<Entry<string>>("SubEntry.Title", op, value).Compile();
Assert.True(predicate(entry));
}

[Theory]
[InlineData(3, "I'm not empty", FilterOperator.IsEmpty, "doesn't matter. ignored")]
[InlineData(3, "Neither am i", FilterOperator.IsEmpty, "")]
[InlineData(3, "", FilterOperator.IsNotEmpty, "")]
[InlineData(3, null, FilterOperator.IsNotEmpty, "")]
public void GetPredicate_ShouldHandleNullOrEmtpyStringOperatorsFalse(int id, string title, FilterOperator op, object value)
{
var entry = new Entry<string>(id, new SubEntry<string>(title));
var predicate = DynamicExpressions.GetPredicate<Entry<string>>("SubEntry.Title", op, value).Compile();
Assert.False(predicate(entry));
}

[Theory]
[MemberData(nameof(ListTestData))]
public void GetPredicate_ShouldHandleEnumerableStringOperators<T>(int id, T title, FilterOperator op, object value)
{
var entry = new Entry<T>(id, new SubEntry<T>(title));
var predicate = DynamicExpressions.GetPredicate<Entry<T>>("SubEntry.Title", op, value).Compile();
Assert.True(predicate(entry));
}

public static IEnumerable<object[]> ListTestData
{
get
{
return new[]{
new object[]{3, new List<string>() { "Title 3" }, FilterOperator.Contains, "Title 3" },
new object[]{3, new List<string>() { "Title 3" }, FilterOperator.NotContains, "Title 5" },
new object[]{3, new Dictionary<string, string> { { "Key 3", "Value 1" } }, FilterOperator.Contains, "Key 3" },
new object[]{3, new Dictionary<string, string> { { "Key 3", "Value 1" } }, FilterOperator.Contains, "Value 1" },
new object[]{3, new Dictionary<string, string> { { "Key 3", "Value 1" } }, FilterOperator.NotContains, "Key 5" },
new object[]{3, new Dictionary<string, string> { { "Key 3", "Value 1" } }, FilterOperator.NotContains, "Value 5" },
new object[]{3, new Dictionary<string, string> { { "Key 3", "Value 1" } }, FilterOperator.ContainsKey, "Key 3" },
new object[]{3, new Dictionary<string, string> { { "Key 3", "Value 1" } }, FilterOperator.ContainsValue, "Value 1" },
new object[]{3, new Dictionary<string, string> { { "Key 3", "Value 1" } }, FilterOperator.NotContainsKey, "Key 5" },
new object[]{3, new Dictionary<string, string> { { "Key 3", "Value 1" } }, FilterOperator.NotContainsValue, "Value 5" }
};
}
}

[Theory]
[InlineData(1, "Title 1", FilterOperator.Contains, 1)]
[InlineData(3, "Title 3", FilterOperator.NotContains, 5)]
[InlineData(1, "1 Title", FilterOperator.StartsWith, 1)]
[InlineData(1, "Title 1", FilterOperator.EndsWith, 1)]
public void GetPredicate_ShouldWork_WhenValueIsNotStringAndOperatorIsStringBased(int id, string title,
FilterOperator op, object value)
{
var entry = new Entry(id, new SubEntry(title));
var predicate = DynamicExpressions.GetPredicate<Entry>("SubEntry.Title", op, value).Compile();
var entry = new Entry<string>(id, new SubEntry<string>(title));
var predicate = DynamicExpressions.GetPredicate<Entry<string>>("SubEntry.Title", op, value).Compile();
Assert.True(predicate(entry));
}
}
Expand Down
22 changes: 11 additions & 11 deletions DynamicExpressions.UnitTests/PropertyGetterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ public void GetPropertyGetter_ShouldThrow_WhenPropertyIsNull()
[Fact]
public void GetPropertyGetter_ShouldReturnCorrectGetter()
{
var entry = new Entry(2);
var entry = new Entry<string>(2);

var getter = DynamicExpressions.GetPropertyGetter<Entry>("Id").Compile();
var getter = DynamicExpressions.GetPropertyGetter<Entry<string>>("Id").Compile();
var id = getter(entry);

Assert.Equal(entry.Id, id);
Expand All @@ -32,13 +32,13 @@ public void GetPropertyGetter_ShouldReturnCorrectGetter()
[Fact]
public void GetPropertyGetter_ShouldBeUsableInQueryableOrderBy()
{
var entries = new List<Entry>
var entries = new List<Entry<string>>
{
new Entry(1),
new Entry(2),
new Entry<string>(1),
new Entry<string>(2),
};

var getter = DynamicExpressions.GetPropertyGetter<Entry>("Id");
var getter = DynamicExpressions.GetPropertyGetter<Entry<string>>("Id");
var sortedEntries = entries.AsQueryable().OrderByDescending(getter).ToList();

Assert.Equal(entries[0], sortedEntries[1]);
Expand All @@ -48,19 +48,19 @@ public void GetPropertyGetter_ShouldBeUsableInQueryableOrderBy()
[Fact]
public void GetPropertyGetter_ShouldThrow_WhenPropertyDoesntExist()
{
var entry = new Entry(2);
var entry = new Entry<string>(2);

var ex = Assert.Throws<ArgumentException>(() => DynamicExpressions.GetPropertyGetter<Entry>("Test"));
var ex = Assert.Throws<ArgumentException>(() => DynamicExpressions.GetPropertyGetter<Entry<string>>("Test"));

Assert.Equal("Instance property 'Test' is not defined for type 'DynamicExpressions.UnitTests.Entry' (Parameter 'propertyName')", ex.Message);
Assert.Equal("Instance property 'Test' is not defined for type 'DynamicExpressions.UnitTests.Entry`1[System.String]' (Parameter 'propertyName')", ex.Message);
}

[Fact]
public void GetPropertyGetter_ShouldHandleNestedProperty()
{
var entry = new Entry(1, new SubEntry("Title"));
var entry = new Entry<string>(1, new SubEntry<string>("Title"));

var getter = DynamicExpressions.GetPropertyGetter<Entry>("SubEntry.Title").Compile();
var getter = DynamicExpressions.GetPropertyGetter<Entry<string>>("SubEntry.Title").Compile();
var value = getter(entry);

Assert.Equal(entry.SubEntry.Title, value);
Expand Down
53 changes: 47 additions & 6 deletions DynamicExpressions/DynamicExpressions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

Expand All @@ -7,17 +10,24 @@ namespace DynamicExpressions
public static class DynamicExpressions
{
private static readonly Type _stringType = typeof(string);
private static readonly Type _enumerableType = typeof(IEnumerable<>);

private static readonly MethodInfo _toStringMethod = typeof(object).GetMethod("ToString");

private static readonly MethodInfo _containsMethod = typeof(string).GetMethod("Contains"
private static readonly MethodInfo _stringContainsMethod = typeof(string).GetMethod("Contains"
, new Type[] { typeof(string) });
private static readonly MethodInfo _containsMethodIgnoreCase = typeof(string).GetMethod("Contains"
private static readonly MethodInfo _stringContainsMethodIgnoreCase = typeof(string).GetMethod("Contains"
, new Type[] { typeof(string), typeof(StringComparison) });
private static readonly MethodInfo _enumerableContainsMethod = typeof(Enumerable).GetMethods().Where(x => string.Equals(x.Name, "Contains", StringComparison.OrdinalIgnoreCase)).Single(x => x.GetParameters().Length == 2).MakeGenericMethod(typeof(string));
private static readonly MethodInfo _dictionaryContainsKeyMethod = typeof(Dictionary<string, string>).GetMethods().Where(x => string.Equals(x.Name, "ContainsKey", StringComparison.OrdinalIgnoreCase)).Single();
private static readonly MethodInfo _dictionaryContainsValueMethod = typeof(Dictionary<string, string>).GetMethods().Where(x => string.Equals(x.Name, "ContainsValue", StringComparison.OrdinalIgnoreCase)).Single();

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

private static readonly MethodInfo _isNullOrEmtpyMethod
= typeof(string).GetMethod("IsNullOrEmpty", new Type[] { typeof(string) });

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

Expand Down Expand Up @@ -49,22 +59,53 @@ private static Expression CreateFilter(MemberExpression prop, FilterOperator op,
{
return op switch
{
FilterOperator.Equals => Expression.Equal(prop, constant),
FilterOperator.Equals => RobustEquals(prop, constant),
FilterOperator.GreaterThan => Expression.GreaterThan(prop, constant),
FilterOperator.LessThan => Expression.LessThan(prop, constant),
FilterOperator.Contains => Expression.Call(prop, _containsMethod, PrepareConstant(constant)),
FilterOperator.ContainsIgnoreCase => Expression.Call(prop, _containsMethodIgnoreCase, PrepareConstant(constant), Expression.Constant(StringComparison.OrdinalIgnoreCase)),
FilterOperator.ContainsIgnoreCase => Expression.Call(prop, _stringContainsMethodIgnoreCase, PrepareConstant(constant), Expression.Constant(StringComparison.OrdinalIgnoreCase)),
FilterOperator.Contains => GetContainsMethodCallExpression(prop, constant),
FilterOperator.NotContains => Expression.Not(GetContainsMethodCallExpression(prop, constant)),
FilterOperator.ContainsKey => Expression.Call(prop, _dictionaryContainsKeyMethod, PrepareConstant(constant)),
FilterOperator.NotContainsKey => Expression.Not(Expression.Call(prop, _dictionaryContainsKeyMethod, PrepareConstant(constant))),
FilterOperator.ContainsValue => Expression.Call(prop, _dictionaryContainsValueMethod, PrepareConstant(constant)),
FilterOperator.NotContainsValue => Expression.Not(Expression.Call(prop, _dictionaryContainsValueMethod, PrepareConstant(constant))),
FilterOperator.StartsWith => Expression.Call(prop, _startsWithMethod, PrepareConstant(constant)),
FilterOperator.EndsWith => Expression.Call(prop, _endsWithMethod, PrepareConstant(constant)),
FilterOperator.DoesntEqual => Expression.NotEqual(prop, constant),
FilterOperator.DoesntEqual => Expression.Not(RobustEquals(prop, constant)),
FilterOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(prop, constant),
FilterOperator.LessThanOrEqual => Expression.LessThanOrEqual(prop, constant),
FilterOperator.IsEmpty => Expression.Call(_isNullOrEmtpyMethod, prop),
FilterOperator.IsNotEmpty => Expression.Not(Expression.Call(_isNullOrEmtpyMethod, prop)),
_ => throw new NotImplementedException()
};
}

private static Expression RobustEquals(MemberExpression prop, ConstantExpression constant)
{
if (prop.Type == typeof(bool) && bool.TryParse(constant.Value.ToString(), out var val))
{
return Expression.Equal(prop, Expression.Constant(val));
}
return Expression.Equal(prop, constant);
}

private static Expression GetContainsMethodCallExpression(MemberExpression prop, ConstantExpression constant)
{
if (prop.Type == _stringType)
return Expression.Call(prop, _stringContainsMethod, PrepareConstant(constant));
else if (prop.Type.GetInterfaces().Contains(typeof(IDictionary)))
return Expression.Or(Expression.Call(prop, _dictionaryContainsKeyMethod, PrepareConstant(constant)), Expression.Call(prop, _dictionaryContainsValueMethod, PrepareConstant(constant)));
else if (prop.Type.GetInterfaces().Contains(typeof(IEnumerable)))
return Expression.Call(_enumerableContainsMethod, prop, PrepareConstant(constant));

throw new NotImplementedException($"{prop.Type} contains is not implemented.");


}

private static Expression PrepareConstant(ConstantExpression constant)
{

if (constant.Type == _stringType)
return constant;

Expand Down
10 changes: 10 additions & 0 deletions DynamicExpressions/DynamicExpressions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>DynamicExpressions</AssemblyName>
<RootNamespace>DynamicExpressions</RootNamespace>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Authors>zHaytam</Authors>
<Company>zHaytam</Company>
<Product>DynamicExpressions.NET</Product>
<PackageId>DynamicExpressions.NET</PackageId>
<RepositoryUrl>https://github.com/zHaytam/DynamicExpressions</RepositoryUrl>
<Description>A dynamic expression builder that can be used to dynamically sort and/or filter LINQ/EF queries</Description>
</PropertyGroup>

<Import Project="Nuget.csproj" Condition="Exists('Nuget.csproj')"></Import>

</Project>
9 changes: 8 additions & 1 deletion DynamicExpressions/FilterOperator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ public enum FilterOperator
LessThan,
LessThanOrEqual,
Contains,
NotContains,
StartsWith,
EndsWith,
ContainsIgnoreCase
ContainsIgnoreCase,
IsEmpty,
IsNotEmpty,
ContainsKey,
NotContainsKey,
ContainsValue,
NotContainsValue
}
}

0 comments on commit 4ce2dfe

Please sign in to comment.