diff --git a/.gitignore b/.gitignore index e645270..adcf094 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,11 @@ *.userosscache *.sln.docstates +.vscode/**/* +nuget.config +.nuspec +Nuget.csproj + # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/DynamicExpressions.UnitTests/Data.cs b/DynamicExpressions.UnitTests/Data.cs index 6b8189f..abdc079 100644 --- a/DynamicExpressions.UnitTests/Data.cs +++ b/DynamicExpressions.UnitTests/Data.cs @@ -1,24 +1,24 @@ namespace DynamicExpressions.UnitTests { - internal class Entry + internal class Entry { - public Entry(int id, SubEntry subEntry = null) + public Entry(int id, SubEntry subEntry = null) { Id = id; SubEntry = subEntry; } public int Id { get; } - public SubEntry SubEntry { get; } + public SubEntry SubEntry { get; } } - internal class SubEntry + internal class SubEntry { - public SubEntry(string title) + public SubEntry(T title) { Title = title; } - public string Title { get; } + public T Title { get; } } } diff --git a/DynamicExpressions.UnitTests/PredicateTests.cs b/DynamicExpressions.UnitTests/PredicateTests.cs index 6596fcd..4b60348 100644 --- a/DynamicExpressions.UnitTests/PredicateTests.cs +++ b/DynamicExpressions.UnitTests/PredicateTests.cs @@ -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 { @@ -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(property, op, value).Compile(); + var entry = new Entry(id, new SubEntry(title)); + var predicate = DynamicExpressions.GetPredicate>(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(int id, T title, + string property, FilterOperator op, object value) + { + var entry = new Entry(id, new SubEntry(title)); + var predicate = DynamicExpressions.GetPredicate>(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("SubEntry.Title", op, value).Compile(); + var entry = new Entry(id, new SubEntry(title)); + var predicate = DynamicExpressions.GetPredicate>("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(id, new SubEntry(title)); + var predicate = DynamicExpressions.GetPredicate>("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(id, new SubEntry(title)); + var predicate = DynamicExpressions.GetPredicate>("SubEntry.Title", op, value).Compile(); + Assert.False(predicate(entry)); + } + + [Theory] + [MemberData(nameof(ListTestData))] + public void GetPredicate_ShouldHandleEnumerableStringOperators(int id, T title, FilterOperator op, object value) + { + var entry = new Entry(id, new SubEntry(title)); + var predicate = DynamicExpressions.GetPredicate>("SubEntry.Title", op, value).Compile(); + Assert.True(predicate(entry)); + } + + public static IEnumerable ListTestData + { + get + { + return new[]{ + new object[]{3, new List() { "Title 3" }, FilterOperator.Contains, "Title 3" }, + new object[]{3, new List() { "Title 3" }, FilterOperator.NotContains, "Title 5" }, + new object[]{3, new Dictionary { { "Key 3", "Value 1" } }, FilterOperator.Contains, "Key 3" }, + new object[]{3, new Dictionary { { "Key 3", "Value 1" } }, FilterOperator.Contains, "Value 1" }, + new object[]{3, new Dictionary { { "Key 3", "Value 1" } }, FilterOperator.NotContains, "Key 5" }, + new object[]{3, new Dictionary { { "Key 3", "Value 1" } }, FilterOperator.NotContains, "Value 5" }, + new object[]{3, new Dictionary { { "Key 3", "Value 1" } }, FilterOperator.ContainsKey, "Key 3" }, + new object[]{3, new Dictionary { { "Key 3", "Value 1" } }, FilterOperator.ContainsValue, "Value 1" }, + new object[]{3, new Dictionary { { "Key 3", "Value 1" } }, FilterOperator.NotContainsKey, "Key 5" }, + new object[]{3, new Dictionary { { "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("SubEntry.Title", op, value).Compile(); + var entry = new Entry(id, new SubEntry(title)); + var predicate = DynamicExpressions.GetPredicate>("SubEntry.Title", op, value).Compile(); Assert.True(predicate(entry)); } } diff --git a/DynamicExpressions.UnitTests/PropertyGetterTests.cs b/DynamicExpressions.UnitTests/PropertyGetterTests.cs index acd316a..be592d0 100644 --- a/DynamicExpressions.UnitTests/PropertyGetterTests.cs +++ b/DynamicExpressions.UnitTests/PropertyGetterTests.cs @@ -21,9 +21,9 @@ public void GetPropertyGetter_ShouldThrow_WhenPropertyIsNull() [Fact] public void GetPropertyGetter_ShouldReturnCorrectGetter() { - var entry = new Entry(2); + var entry = new Entry(2); - var getter = DynamicExpressions.GetPropertyGetter("Id").Compile(); + var getter = DynamicExpressions.GetPropertyGetter>("Id").Compile(); var id = getter(entry); Assert.Equal(entry.Id, id); @@ -32,13 +32,13 @@ public void GetPropertyGetter_ShouldReturnCorrectGetter() [Fact] public void GetPropertyGetter_ShouldBeUsableInQueryableOrderBy() { - var entries = new List + var entries = new List> { - new Entry(1), - new Entry(2), + new Entry(1), + new Entry(2), }; - var getter = DynamicExpressions.GetPropertyGetter("Id"); + var getter = DynamicExpressions.GetPropertyGetter>("Id"); var sortedEntries = entries.AsQueryable().OrderByDescending(getter).ToList(); Assert.Equal(entries[0], sortedEntries[1]); @@ -48,19 +48,19 @@ public void GetPropertyGetter_ShouldBeUsableInQueryableOrderBy() [Fact] public void GetPropertyGetter_ShouldThrow_WhenPropertyDoesntExist() { - var entry = new Entry(2); + var entry = new Entry(2); - var ex = Assert.Throws(() => DynamicExpressions.GetPropertyGetter("Test")); + var ex = Assert.Throws(() => DynamicExpressions.GetPropertyGetter>("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(1, new SubEntry("Title")); - var getter = DynamicExpressions.GetPropertyGetter("SubEntry.Title").Compile(); + var getter = DynamicExpressions.GetPropertyGetter>("SubEntry.Title").Compile(); var value = getter(entry); Assert.Equal(entry.SubEntry.Title, value); diff --git a/DynamicExpressions/DynamicExpressions.cs b/DynamicExpressions/DynamicExpressions.cs index 65ca2fb..692ff0a 100644 --- a/DynamicExpressions/DynamicExpressions.cs +++ b/DynamicExpressions/DynamicExpressions.cs @@ -1,4 +1,7 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -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).GetMethods().Where(x => string.Equals(x.Name, "ContainsKey", StringComparison.OrdinalIgnoreCase)).Single(); + private static readonly MethodInfo _dictionaryContainsValueMethod = typeof(Dictionary).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) }); @@ -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; diff --git a/DynamicExpressions/DynamicExpressions.csproj b/DynamicExpressions/DynamicExpressions.csproj index 5793b87..e3a27c1 100644 --- a/DynamicExpressions/DynamicExpressions.csproj +++ b/DynamicExpressions/DynamicExpressions.csproj @@ -4,6 +4,16 @@ netstandard2.1 DynamicExpressions DynamicExpressions + true + MIT + zHaytam + zHaytam + DynamicExpressions.NET + DynamicExpressions.NET + https://github.com/zHaytam/DynamicExpressions + A dynamic expression builder that can be used to dynamically sort and/or filter LINQ/EF queries + + diff --git a/DynamicExpressions/FilterOperator.cs b/DynamicExpressions/FilterOperator.cs index 2b5eb10..140119a 100644 --- a/DynamicExpressions/FilterOperator.cs +++ b/DynamicExpressions/FilterOperator.cs @@ -9,8 +9,15 @@ public enum FilterOperator LessThan, LessThanOrEqual, Contains, + NotContains, StartsWith, EndsWith, - ContainsIgnoreCase + ContainsIgnoreCase, + IsEmpty, + IsNotEmpty, + ContainsKey, + NotContainsKey, + ContainsValue, + NotContainsValue } }