From 015c0ed0e8c8157544c22464d541ec35bcae3ca6 Mon Sep 17 00:00:00 2001 From: Martin Oom Date: Mon, 6 May 2019 09:52:00 +0200 Subject: [PATCH] Adds PredicateGenerator (#1) --- README.md | 45 +++++ .../Infrastructure/XunitSerializable.cs | 23 +++ .../Internals/EnumerableExtensionsTests.cs | 83 +++++++++ .../Internals/TypeExtensionsTests.cs | 144 ++++++++++++++++ .../PredicateGeneratorTests.cs | 73 ++++++++ ...erablePropertyTypePredicateHandlerTests.cs | 137 +++++++++++++++ ...SimplePropertyTypePredicateHandlerTests.cs | 158 ++++++++++++++++++ .../ExpressionExtensions.cs | 3 + .../Internals/EnumerableExtensions.cs | 19 +++ .../Internals/TypeExtensions.cs | 101 +++++++++++ .../IPropertyPredicateHandler.cs | 30 ++++ .../PredicateGenerator/PredicateGenerator.cs | 103 ++++++++++++ ...eEnumerablePropertyTypePredicateHandler.cs | 64 +++++++ .../SimplePropertyTypePredicateHandler.cs | 58 +++++++ 14 files changed, 1041 insertions(+) create mode 100644 src/Spinit.Expressions.UnitTests/Infrastructure/XunitSerializable.cs create mode 100644 src/Spinit.Expressions.UnitTests/Internals/EnumerableExtensionsTests.cs create mode 100644 src/Spinit.Expressions.UnitTests/Internals/TypeExtensionsTests.cs create mode 100644 src/Spinit.Expressions.UnitTests/PredicateGenerator/PredicateGeneratorTests.cs create mode 100644 src/Spinit.Expressions.UnitTests/PredicateGenerator/SimpleEnumerablePropertyTypePredicateHandlerTests.cs create mode 100644 src/Spinit.Expressions.UnitTests/PredicateGenerator/SimplePropertyTypePredicateHandlerTests.cs create mode 100644 src/Spinit.Expressions/Internals/EnumerableExtensions.cs create mode 100644 src/Spinit.Expressions/Internals/TypeExtensions.cs create mode 100644 src/Spinit.Expressions/PredicateGenerator/IPropertyPredicateHandler.cs create mode 100644 src/Spinit.Expressions/PredicateGenerator/PredicateGenerator.cs create mode 100644 src/Spinit.Expressions/PredicateGenerator/SimpleEnumerablePropertyTypePredicateHandler.cs create mode 100644 src/Spinit.Expressions/PredicateGenerator/SimplePropertyTypePredicateHandler.cs diff --git a/README.md b/README.md index d9aea34..8a58120 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,48 @@ Example: Expression> stringExpression = a => a == "123"; var myClassExpression = stringExpression.RemapTo(myClass => myClass.Id); // provide a path // myClassExpression: myClass => myClass.Id == "123" + +### Predicate generator + +This is a utility class for generating an expression on a class given another "filter" class. + +Example scenario: You have a webapi using some ORM (NHibernate, EntityFramework, Spinit.CosmosDb) and want to allow api users to supply a filter for your data. + +Metacode for this scenario: + + public class MyEntity + { + public string Id { get; set; } + ... + public MyStatusEnum Status { get; set; } + public string Category { get; set; } + ... + } + + public class MyEntityFilter + { + public MyStatusEnum? Status { get; set; } + public IEnumerable Category { get; set; } + } + + public MyApiController : BaseController + { + private readonly IQueryable _entities; + + // WebApi action method + public IEnumerable Get(MyEntityFilter filter) + { + var predicate = new PredicateBuilder().Generate(filter); + // if filter.Status and filter.Category is set the resulting expression looks like: + // x => x.Status == filter.Status && filter.Category.Contains(x.Category) + return _entities.Where(predicate); + } + } + + +Out of the box the `PredicateGenerator` handles "simple types", eg value types and strings. You can supply your own `IPropertyPredicateHandler` and add it using `PredicateGenerator.AddHandler`. + +Conventions: + * The property name of the filter class and the entity must match. + * When using an enumerable on the filter class `Contains` is used, eg it should match any of the supplied values (OR). + * If the filter value is null (or an empty enumerable) no predicate is applied for the current property \ No newline at end of file diff --git a/src/Spinit.Expressions.UnitTests/Infrastructure/XunitSerializable.cs b/src/Spinit.Expressions.UnitTests/Infrastructure/XunitSerializable.cs new file mode 100644 index 0000000..493ca1e --- /dev/null +++ b/src/Spinit.Expressions.UnitTests/Infrastructure/XunitSerializable.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using Xunit.Abstractions; + +namespace Spinit.Expressions.UnitTests.Infrastructure +{ + public class XunitSerializable : IXunitSerializable + { + private static readonly JsonSerializerSettings settings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }; + + public void Deserialize(IXunitSerializationInfo info) + { + JsonConvert.PopulateObject(info.GetValue("json"), this, settings); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue("json", JsonConvert.SerializeObject(this, settings)); + } + } +} diff --git a/src/Spinit.Expressions.UnitTests/Internals/EnumerableExtensionsTests.cs b/src/Spinit.Expressions.UnitTests/Internals/EnumerableExtensionsTests.cs new file mode 100644 index 0000000..92a13a8 --- /dev/null +++ b/src/Spinit.Expressions.UnitTests/Internals/EnumerableExtensionsTests.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Spinit.Expressions.UnitTests.Infrastructure; +using Xunit; + +namespace Spinit.Expressions.UnitTests.Internals +{ + public class EnumerableExtensionsTests + { + [Theory] + [MemberData(nameof(GetScenarios))] + public void IsEmptyShouldReturnExpectedValue(Scenario scenario) + { + var result = scenario.Enumerable.IsEmpty(); + Assert.Equal(scenario.ExpectedResult, result); + } + + public static TheoryData GetScenarios() + { + return new TheoryData + { + new Scenario + { + Enumerable = Array.Empty(), + ExpectedResult = true + }, + new Scenario + { + Enumerable = Array.Empty(), + ExpectedResult = true + }, + new Scenario + { + Enumerable = new Collection(), + ExpectedResult = true + }, + new Scenario + { + Enumerable = new List(), + ExpectedResult = true + }, + new Scenario + { + Enumerable = new HashSet(), + ExpectedResult = true + }, + new Scenario + { + Enumerable = new bool[] { false }, + ExpectedResult = false + }, + new Scenario + { + Enumerable = new string[] { "" }, + ExpectedResult = false + }, + new Scenario + { + Enumerable = new Collection{ 1M }, + ExpectedResult = false + }, + new Scenario + { + Enumerable = new List{ 0, 1, 2 }, + ExpectedResult = false + }, + new Scenario + { + Enumerable = new HashSet{ 123 }, + ExpectedResult = false + } + }; + } + + public class Scenario : XunitSerializable + { + public IEnumerable Enumerable { get; set; } + public bool ExpectedResult { get; set; } + } + } +} diff --git a/src/Spinit.Expressions.UnitTests/Internals/TypeExtensionsTests.cs b/src/Spinit.Expressions.UnitTests/Internals/TypeExtensionsTests.cs new file mode 100644 index 0000000..4dde68a --- /dev/null +++ b/src/Spinit.Expressions.UnitTests/Internals/TypeExtensionsTests.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace Spinit.Expressions.UnitTests +{ + public class TypeExtensionsTests + { + public class IsNullable + { + [Fact] + public void StringIsNotNullable() + { + Assert.False(typeof(string).IsNullable()); + } + + [Fact] + public void IntIsNotNullable() + { + Assert.False(typeof(int).IsNullable()); + } + + [Fact] + public void NullabeIntIsNullable() + { + Assert.True(typeof(int?).IsNullable()); + Assert.True(typeof(Nullable).IsNullable()); + } + + [Fact] + public void ClassDoesNotImplementNullable() + { + Assert.False(typeof(TestClass).IsNullable()); + } + +#if false + // only for C#8, nullable string + [Fact] + public void NullabestringIsNullable() + { + Assert.True(typeof(string?).IsNullable()); + Assert.True(typeof(Nullable).IsNullable()); + } +#endif + public class TestClass + { + public int IntProperty1 { get; set; } + } + } + + public class IsNullableOfT + { + [Fact] + public void NullableIntIsNotNullableOfBool() + { + Assert.False(typeof(int?).IsNullableOf()); + } + + [Fact] + public void NullableIntIsNullableOfInt() + { + Assert.True(typeof(int?).IsNullableOf()); + } + } + + public class IsNullableOf + { + [Fact] + public void NullableIntIsNullableOfPrimitiveType() + { + Assert.True(typeof(int?).IsNullableOf(x => x.IsPrimitive)); + } + + [Fact] + public void NullableIntIsNullableOfInt() + { + Assert.True(typeof(int?).IsNullableOf(x => x == typeof(int))); + } + } + + public class IsEnumerable + { + [Fact] + public void StringIsAnEnumerable() + { + Assert.True(typeof(string).IsEnumerable()); + } + + [Fact] + public void ICollectionOfStringIsAnEnumerable() + { + Assert.True(typeof(ICollection).IsEnumerable()); + } + + [Fact] + public void ListOfStringIsAnEnumerable() + { + Assert.True(typeof(List).IsEnumerable()); + } + + [Fact] + public void BoolArrayIsAnEnumerable() + { + Assert.True(typeof(bool[]).IsEnumerable()); + } + } + + public class IsEnumerableOfT + { + [Fact] + public void StringIsAnEnumerableOfChar() + { + Assert.True(typeof(string).IsEnumerableOf()); + } + + [Fact] + public void ICollectionOfStringIsAnEnumerableOfString() + { + Assert.True(typeof(ICollection).IsEnumerableOf()); + } + + [Fact] + public void ListOfStringIsAnEnumerableOfString() + { + Assert.True(typeof(List).IsEnumerableOf()); + } + } + + public class IsEnumerableOf + { + [Fact] + public void IntArrayIsEnumerableOfPrimitiveType() + { + Assert.True(typeof(int[]).IsEnumerableOf(x => x.IsPrimitive)); + } + + [Fact] + public void StringListIsEnumerableOfString() + { + Assert.True(typeof(List).IsEnumerableOf(x => x == typeof(string))); + } + } + } +} diff --git a/src/Spinit.Expressions.UnitTests/PredicateGenerator/PredicateGeneratorTests.cs b/src/Spinit.Expressions.UnitTests/PredicateGenerator/PredicateGeneratorTests.cs new file mode 100644 index 0000000..db422eb --- /dev/null +++ b/src/Spinit.Expressions.UnitTests/PredicateGenerator/PredicateGeneratorTests.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Xunit; + +namespace Spinit.Expressions.UnitTests +{ + public class PredicateGeneratorTests + { + [Fact] + public void TestIdPredicate() + { + var filter = new TodoFilter { Id = 123 }; + var predicateGenerator = new PredicateGenerator(); + var predicate = predicateGenerator.Generate(filter); + //var expectedPredicate = ExpressionExtensions.Expression(x => x.Id == 123); + Expression> expectedPredicate = x => x.Id == 123; + Assert.Equal(expectedPredicate.ToString(), predicate.ToString()); + } + + [Fact] + public void TestTypePredicate() + { + var filter = new TodoFilter { Type = new[] { "a", "b" } }; + var predicateGenerator = new PredicateGenerator(); + var predicate = predicateGenerator.Generate(filter); + + var items = new List + { + new Todo { Type = "a" }, + new Todo { Type = "b" }, + new Todo { Type = "c" }, + }; + var filteredItems = items.AsQueryable().Where(predicate).ToList(); + Assert.DoesNotContain(filteredItems, x => x.Type == "c"); + } + + //[Fact] + //public void TestTagsPredicate() + //{ + // var filter = new TodoFilter { Tags = new[] { "a", "b" } }; + // var predicateGenerator = new PredicateGenerator(); + // var predicate = predicateGenerator.Generate(filter); + + // var items = new List + // { + // new Todo {Tags = new [] { "a" } }, + // new Todo {Tags = new [] { "b" } }, + // new Todo {Tags = new [] { "c" } }, + // }; + // var filteredItems = items.AsQueryable().Where(predicate).ToList(); + // Assert.DoesNotContain(filteredItems, x => x.Tags.Contains("c")); + //} + + public class TodoFilter + { + public int? Id { get; set; } + + public IEnumerable Type { get; set; } + + // TODO: add OperatorAttribute (AND | OR) + implement IEnumerable support on target side + //public IEnumerable Tags { get; set; } + } + + public class Todo + { + public int Id { get; set; } + public string Type { get; set; } + //public IEnumerable Tags { get; set; } + } + } +} diff --git a/src/Spinit.Expressions.UnitTests/PredicateGenerator/SimpleEnumerablePropertyTypePredicateHandlerTests.cs b/src/Spinit.Expressions.UnitTests/PredicateGenerator/SimpleEnumerablePropertyTypePredicateHandlerTests.cs new file mode 100644 index 0000000..f659654 --- /dev/null +++ b/src/Spinit.Expressions.UnitTests/PredicateGenerator/SimpleEnumerablePropertyTypePredicateHandlerTests.cs @@ -0,0 +1,137 @@ +using System; +using Xunit; + +namespace Spinit.Expressions.UnitTests.PredicateGenerator +{ + public class SimpleEnumerablePropertyTypePredicateHandlerTests + { + public class CanHandle + { + [Theory] + [MemberData(nameof(GetPropertyNames), typeof(FilterWithSimpleEnumerableProps), MemberType = typeof(SimpleEnumerablePropertyTypePredicateHandlerTests))] + public void AssertHandleable(string propertyName) + { + var handler = new SimpleEnumerablePropertyTypePredicateHandler(); + var property = typeof(FilterWithSimpleEnumerableProps).GetProperty(propertyName); + var result = handler.CanHandle(property); + Assert.True(result); + } + + [Theory] + [MemberData(nameof(GetPropertyNames), typeof(FilterWithSimpleEnumerableProps), MemberType = typeof(SimpleEnumerablePropertyTypePredicateHandlerTests))] + public void AssertHandleableWhenNull(string propertyName) + { + var handler = new SimpleEnumerablePropertyTypePredicateHandler(); + var property = typeof(FilterWithSimpleEnumerableProps).GetProperty(propertyName); + var result = handler.CanHandle(property); + Assert.True(result); + } + + [Theory] + [MemberData(nameof(GetPropertyNames), typeof(FilterWithNonSimpleEnumerableProps), MemberType = typeof(SimpleEnumerablePropertyTypePredicateHandlerTests))] + public void AssertNonHandleable(string propertyName) + { + var handler = new SimpleEnumerablePropertyTypePredicateHandler(); + var property = typeof(FilterWithNonSimpleEnumerableProps).GetProperty(propertyName); + var result = handler.CanHandle(property); + Assert.False(result); + } + } + + public class Handle + { + [Theory] + [MemberData(nameof(GetPropertyNames), typeof(FilterWithSimpleEnumerableProps), MemberType = typeof(SimpleEnumerablePropertyTypePredicateHandlerTests))] + public void HandleShouldReturnNullForEmptyProperties(string propertyName) + { + var handler = new SimpleEnumerablePropertyTypePredicateHandler(); + var property = typeof(FilterWithSimpleEnumerableProps).GetProperty(propertyName); + var filter = new FilterWithSimpleEnumerableProps(); + var result = handler.Handle(filter, property); + Assert.Null(result); + } + + [Theory] + [MemberData(nameof(GetPropertyNames), typeof(FilterWithSimpleEnumerableProps), MemberType = typeof(SimpleEnumerablePropertyTypePredicateHandlerTests))] + public void HandleShouldReturnExpectedResult(string propertyName) + { + var handler = new SimpleEnumerablePropertyTypePredicateHandler(); + var property = typeof(FilterWithSimpleEnumerableProps).GetProperty(propertyName); + var filter = new FilterWithSimpleEnumerableProps(); + var underlyingType = property.PropertyType.GetEnumerableUnderlyingType(); + var array = Array.CreateInstance(underlyingType, 1); + array.Initialize(); + property.SetValue(filter, array); + var result = handler.Handle(filter, property); + Assert.Matches($@"x => value\(.*\[\]\).Contains\(x\.{property.Name}\)", result.ToString()); + } + } + + public static TheoryData GetPropertyNames(Type type) + { + var result = new TheoryData(); + foreach (var property in type.GetProperties()) + { + result.Add(property.Name); + } + return result; + } + + public class FilterWithSimpleEnumerableProps + { + public bool[] BoolProp { get; set; } + public byte[] ByteProp { get; set; } + public sbyte[] SByteProp { get; set; } + public char[] CharProp { get; set; } + public decimal[] DecimalProp { get; set; } + public double[] DoubleProp { get; set; } + public float[] FloatProp { get; set; } + public int[] IntProp { get; set; } + public uint[] UIntProp { get; set; } + public long[] LongProp { get; set; } + public ulong[] ULongProp { get; set; } + public short[] ShortProp { get; set; } + public ushort[] UShortProp { get; set; } + public string[] StringProp { get; set; } + public MyEnum[] EnumProp { get; set; } + public DateTime[] DateTimeProp { get; set; } + public TimeSpan[] TimeSpanProp { get; set; } + public Guid[] GuidProp { get; set; } + } + + public class FilterWithNonSimpleEnumerableProps + { + public dynamic[] DynamicProp { get; set; } + public Tuple[] TupleProp { get; set; } + public Entity[] EntityProp { get; set; } + } + + public enum MyEnum + { + Value1, + Value2 + } + + public class Entity + { + public bool BoolProp { get; set; } + public byte ByteProp { get; set; } + public sbyte SByteProp { get; set; } + public char CharProp { get; set; } + public decimal DecimalProp { get; set; } + public double DoubleProp { get; set; } + public float FloatProp { get; set; } + public int IntProp { get; set; } + public uint UIntProp { get; set; } + public long LongProp { get; set; } + public ulong ULongProp { get; set; } + public short ShortProp { get; set; } + public ushort UShortProp { get; set; } + public string StringProp { get; set; } + public MyEnum EnumProp { get; set; } + public DateTime DateTimeProp { get; set; } + public TimeSpan TimeSpanProp { get; set; } + public Guid GuidProp { get; set; } + } + } +} diff --git a/src/Spinit.Expressions.UnitTests/PredicateGenerator/SimplePropertyTypePredicateHandlerTests.cs b/src/Spinit.Expressions.UnitTests/PredicateGenerator/SimplePropertyTypePredicateHandlerTests.cs new file mode 100644 index 0000000..ba2d715 --- /dev/null +++ b/src/Spinit.Expressions.UnitTests/PredicateGenerator/SimplePropertyTypePredicateHandlerTests.cs @@ -0,0 +1,158 @@ +using System; +using Xunit; + +namespace Spinit.Expressions.UnitTests.PredicateGenerator +{ + public class SimplePropertyTypePredicateHandlerTests + { + public class CanHandle + { + [Theory] + [MemberData(nameof(GetPropertyNames), typeof(FilterWithSimpleProps), MemberType = typeof(SimplePropertyTypePredicateHandlerTests))] + public void AssertHandleable(string propertyName) + { + var handler = new SimplePropertyTypePredicateHandler(); + var property = typeof(FilterWithSimpleProps).GetProperty(propertyName); + var result = handler.CanHandle(property); + Assert.True(result); + } + + [Theory] + [MemberData(nameof(GetPropertyNames), typeof(FilterWithNullableSimpleProps), MemberType = typeof(SimplePropertyTypePredicateHandlerTests))] + public void AssertHandleableWhenNullable(string propertyName) + { + var handler = new SimplePropertyTypePredicateHandler(); + var property = typeof(FilterWithNullableSimpleProps).GetProperty(propertyName); + var result = handler.CanHandle(property); + Assert.True(result); + } + + [Theory] + [MemberData(nameof(GetPropertyNames), typeof(FilterWithNonSimpleProps), MemberType = typeof(SimplePropertyTypePredicateHandlerTests))] + public void AssertNonHandleable(string propertyName) + { + var handler = new SimplePropertyTypePredicateHandler(); + var property = typeof(FilterWithNonSimpleProps).GetProperty(propertyName); + var result = handler.CanHandle(property); + Assert.False(result); + } + } + + public class Handle + { + [Theory] + [MemberData(nameof(GetPropertyNames), typeof(FilterWithNullableSimpleProps), MemberType = typeof(SimplePropertyTypePredicateHandlerTests))] + public void HandleShouldReturnNullForEmptyProperties(string propertyName) + { + var handler = new SimplePropertyTypePredicateHandler(); + var property = typeof(FilterWithNullableSimpleProps).GetProperty(propertyName); + var filter = new FilterWithNullableSimpleProps(); + var result = handler.Handle(filter, property); + Assert.Null(result); + } + + [Theory] + [MemberData(nameof(GetPropertyNames), typeof(FilterWithSimpleProps), MemberType = typeof(SimplePropertyTypePredicateHandlerTests))] + public void HandleShouldReturnExpectedResult(string propertyName) + { + var handler = new SimplePropertyTypePredicateHandler(); + var property = typeof(FilterWithSimpleProps).GetProperty(propertyName); + var filter = new FilterWithSimpleProps + { + StringProp = "" // needs to be set, since default == null + }; + var result = handler.Handle(filter, property); + Assert.Matches($@"x => \(x\.{property.Name} == .+\)", result.ToString()); + } + } + + public static TheoryData GetPropertyNames(Type type) + { + var result = new TheoryData(); + foreach (var property in type.GetProperties()) + { + result.Add(property.Name); + } + return result; + } + + public class FilterWithSimpleProps + { + public bool BoolProp { get; set; } + public byte ByteProp { get; set; } + public sbyte SByteProp { get; set; } + public char CharProp { get; set; } + public decimal DecimalProp { get; set; } + public double DoubleProp { get; set; } + public float FloatProp { get; set; } + public int IntProp { get; set; } + public uint UIntProp { get; set; } + public long LongProp { get; set; } + public ulong ULongProp { get; set; } + public short ShortProp { get; set; } + public ushort UShortProp { get; set; } + public string StringProp { get; set; } + public MyEnum EnumProp { get; set; } + public DateTime DateTimeProp { get; set; } + public TimeSpan TimeSpanProp { get; set; } + public Guid GuidProp { get; set; } + } + + public class FilterWithNullableSimpleProps + { + public bool? BoolProp { get; set; } + public byte? ByteProp { get; set; } + public sbyte? SByteProp { get; set; } + public char? CharProp { get; set; } + public decimal? DecimalProp { get; set; } + public double? DoubleProp { get; set; } + public float? FloatProp { get; set; } + public int? IntProp { get; set; } + public uint? UIntProp { get; set; } + public long? LongProp { get; set; } + public ulong? ULongProp { get; set; } + public short? ShortProp { get; set; } + public ushort? UShortProp { get; set; } + public string StringProp { get; set; } + public MyEnum? EnumProp { get; set; } + public DateTime? DateTimeProp { get; set; } + public TimeSpan? TimeSpanProp { get; set; } + public Guid? GuidProp { get; set; } + } + + public class FilterWithNonSimpleProps + { + public dynamic DynamicProp { get; set; } + public Tuple TupleProp { get; set; } + public Entity EntityProp { get; set; } + } + + public enum MyEnum + { + Value1, + Value2 + } + + public class Entity + { + public bool BoolProp { get; set; } + public byte ByteProp { get; set; } + public sbyte SByteProp { get; set; } + public char CharProp { get; set; } + public decimal DecimalProp { get; set; } + public double DoubleProp { get; set; } + public float FloatProp { get; set; } + public int IntProp { get; set; } + public uint UIntProp { get; set; } + public long LongProp { get; set; } + public ulong ULongProp { get; set; } + public short ShortProp { get; set; } + public ushort UShortProp { get; set; } + public string StringProp { get; set; } + public MyEnum EnumProp { get; set; } + public DateTime DateTimeProp { get; set; } + public TimeSpan TimeSpanProp { get; set; } + public Guid GuidProp { get; set; } + } + } +} diff --git a/src/Spinit.Expressions/ExpressionExtensions.cs b/src/Spinit.Expressions/ExpressionExtensions.cs index 0224b63..5cb5b3a 100644 --- a/src/Spinit.Expressions/ExpressionExtensions.cs +++ b/src/Spinit.Expressions/ExpressionExtensions.cs @@ -5,6 +5,9 @@ namespace Spinit.Expressions { + /// + /// Contains expression extensions + /// public static class ExpressionExtensions { /// diff --git a/src/Spinit.Expressions/Internals/EnumerableExtensions.cs b/src/Spinit.Expressions/Internals/EnumerableExtensions.cs new file mode 100644 index 0000000..d9bb4f3 --- /dev/null +++ b/src/Spinit.Expressions/Internals/EnumerableExtensions.cs @@ -0,0 +1,19 @@ +using System.Collections; + +namespace Spinit.Expressions +{ + internal static class EnumerableExtensions + { + internal static bool IsEmpty(this IEnumerable source) + { + if (source != null) + { + foreach (var item in source) + { + return false; + } + } + return true; + } + } +} diff --git a/src/Spinit.Expressions/Internals/TypeExtensions.cs b/src/Spinit.Expressions/Internals/TypeExtensions.cs new file mode 100644 index 0000000..d712ce6 --- /dev/null +++ b/src/Spinit.Expressions/Internals/TypeExtensions.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Spinit.Expressions +{ + internal static class TypeExtensions + { + /// + /// Checks if a type is a + /// + /// + /// + internal static bool IsNullable(this Type type) + { + return type.IsNullableOf(x => true); + } + + /// + /// Checks if a type is a + /// + /// + /// + /// + internal static bool IsNullableOf(this Type type) + { + return type.IsNullableOf(x => x == typeof(T)); + } + + /// + /// Checks if a type is a where T matches the + /// + /// + /// + /// + internal static bool IsNullableOf(this Type type, Func underlyingTypePredicate) + { + var underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType == null) + return false; + if (underlyingTypePredicate == null) + return true; + return underlyingTypePredicate(underlyingType); + } + + /// + /// Checks if a type is a + /// + /// + /// + internal static bool IsEnumerable(this Type type) + { + return type.IsEnumerableOf(x => true); + } + + /// + /// Checks if a type is a + /// + /// + /// + /// + internal static bool IsEnumerableOf(this Type type) + { + return type.IsEnumerableOf(x => x == typeof(T)); + } + + /// + /// Checks if a type is a where T matches the + /// + /// + /// + /// + internal static bool IsEnumerableOf(this Type type, Func underlyingTypePredicate) + { + var underlyingType = type.GetEnumerableUnderlyingType(); + if (underlyingType == null) + return false; + if (underlyingTypePredicate == null) + return true; + return underlyingTypePredicate(underlyingType); + } + + internal static Type GetEnumerableUnderlyingType(this Type type) + { + if (type.IsArray) + return type.GetElementType(); + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + return type.GetGenericArguments()[0]; + + var underlyingTypes = type.GetInterfaces() + .Where( + t => t.IsGenericType && + t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + .Select(t => t.GenericTypeArguments[0]); + if (!underlyingTypes.Any()) + return null; + return underlyingTypes.Single(); // TODO: throw better exception if type implements multiple IEnumerable + } + } +} diff --git a/src/Spinit.Expressions/PredicateGenerator/IPropertyPredicateHandler.cs b/src/Spinit.Expressions/PredicateGenerator/IPropertyPredicateHandler.cs new file mode 100644 index 0000000..410014e --- /dev/null +++ b/src/Spinit.Expressions/PredicateGenerator/IPropertyPredicateHandler.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Spinit.Expressions +{ + /// + /// Base interface for all handlers that can construct a predicate for a property. + /// + /// The source filter type + /// The type that the predicate should operate on + /// + public interface IPropertyPredicateHandler + { + /// + /// Returns true if the property can be handled by this handler. + /// + /// The property to check + /// True if the handler can handle this property + bool CanHandle(PropertyInfo propertyInfo); + + /// + /// Builds a predicate for the specified property. + /// + /// The source filter + /// The property + /// Returns a predicate or null if filter not set. + Expression> Handle(TSource source, PropertyInfo propertyInfo); + } +} diff --git a/src/Spinit.Expressions/PredicateGenerator/PredicateGenerator.cs b/src/Spinit.Expressions/PredicateGenerator/PredicateGenerator.cs new file mode 100644 index 0000000..64483d8 --- /dev/null +++ b/src/Spinit.Expressions/PredicateGenerator/PredicateGenerator.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Spinit.Expressions +{ + /// + /// Utility class for generating a predicate given a filter/dto. + /// + /// Handles simple properties (eg value types and strings) custom handlers could be added via + /// + /// + /// + /// Intended use is to generate an expression for use in a ORM framework from a class normaly supplied by api/mvc. + /// + /// + /// + public class PredicateGenerator + where TSource : class + where TTarget : class + { + private readonly IList> _propertyPredicateHandlers; + + /// + /// Initializes a new instance of the class that + /// contains the default simple propertytype handlers. + /// + public PredicateGenerator() + { + _propertyPredicateHandlers = new List>(); + AddHandler>(); + AddHandler>(); + } + + /// + /// Adds a handler that can convert a property to an predicate. + /// + /// + /// + public PredicateGenerator AddHandler() + where THandler : class, IPropertyPredicateHandler, new() + { + var handler = (IPropertyPredicateHandler)Activator.CreateInstance(); + _propertyPredicateHandlers.Add(handler); + return this; + } + + /// + /// Adds a handler that can convert a property to an predicate. + /// + /// + /// + /// + public PredicateGenerator AddHandler(THandler handler) + where THandler : class, IPropertyPredicateHandler + { + _propertyPredicateHandlers.Add(handler); + return this; + } + + /// + /// Generates an predicate that operates on the target type. + /// + /// + /// Handlers will be executed in reverse order for each property, if no handler handles the specific property an exception will be thrown. + /// + /// + /// + /// + public Expression> Generate(TSource source) + { + var predicates = new List>>(); + foreach (var property in source.GetType().GetProperties()) + { + var predicate = GenerateProperyPredicate(source, property); + if (predicate != null) + predicates.Add(predicate); + } + // TODO: return predicates.Combine(Operator.And); + if (!predicates.Any()) + return null; + var result = predicates.First(); + foreach (var predicate in predicates.Skip(1)) + { + result = result.And(predicate); + } + return result; + } + + private Expression> GenerateProperyPredicate(TSource source, PropertyInfo property) + { + foreach (var propertyPredicateHandler in _propertyPredicateHandlers.Reverse()) + { + if (!propertyPredicateHandler.CanHandle(property)) + continue; + return propertyPredicateHandler.Handle(source, property); + } + throw new Exception($"No registered generator for type {property.PropertyType.Name}"); + } + } +} diff --git a/src/Spinit.Expressions/PredicateGenerator/SimpleEnumerablePropertyTypePredicateHandler.cs b/src/Spinit.Expressions/PredicateGenerator/SimpleEnumerablePropertyTypePredicateHandler.cs new file mode 100644 index 0000000..4591244 --- /dev/null +++ b/src/Spinit.Expressions/PredicateGenerator/SimpleEnumerablePropertyTypePredicateHandler.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Spinit.Expressions +{ + /// + /// Handles predicate for enumerables of simple types (eg value types and strings) + /// + public class SimpleEnumerablePropertyTypePredicateHandler : IPropertyPredicateHandler + { + /// + /// Returns true if the property type is an enumerable of simple type (eg value types and strings) + /// + /// + /// + public bool CanHandle(PropertyInfo propertyInfo) + { + var propertyType = propertyInfo.PropertyType; + return propertyType != typeof(string) && // string is an enumerable of char, should not be handled + propertyType.IsEnumerableOf(x => x.IsValueType || x == typeof(string)); + } + + /// + /// Returns null is the enumerable property is empty or a predicate on + /// + /// + /// + /// + public Expression> Handle(TSource source, PropertyInfo propertyInfo) + { + var propertyValue = propertyInfo.GetValue(source); + if (propertyValue == null || ((IEnumerable)propertyValue).IsEmpty()) + return null; + + var propertyType = propertyInfo.PropertyType; + var targetType = typeof(TTarget); + var targetPropery = targetType.GetProperty(propertyInfo.Name); // TODO: add support for attribute for setting target property name + if (targetPropery == null) + throw new Exception($"{targetType.Name} does not contain a property named {propertyInfo.Name}"); + + var genericEnumerableType = propertyType.GetEnumerableUnderlyingType(); + + if (targetPropery.PropertyType != genericEnumerableType) + throw new Exception($"{targetPropery.Name} must be of type {genericEnumerableType.Name}"); + + var parameterExpression = Expression.Parameter(targetType, "x"); + var propertyExpression = Expression.Property(parameterExpression, targetPropery); + var valuesExpression = Expression.Constant(propertyValue, propertyType); + + var containsMethodInfo = typeof(Enumerable). + GetMethods(). + Where(x => x.Name == "Contains"). + Single(x => x.GetParameters().Length == 2). + MakeGenericMethod(genericEnumerableType); + + var containsMethodExpression = Expression.Call(containsMethodInfo, valuesExpression, propertyExpression); + var result = Expression.Lambda>(containsMethodExpression, parameterExpression); + return result; + } + } +} diff --git a/src/Spinit.Expressions/PredicateGenerator/SimplePropertyTypePredicateHandler.cs b/src/Spinit.Expressions/PredicateGenerator/SimplePropertyTypePredicateHandler.cs new file mode 100644 index 0000000..7d9369f --- /dev/null +++ b/src/Spinit.Expressions/PredicateGenerator/SimplePropertyTypePredicateHandler.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Spinit.Expressions +{ + /// + /// Handles predicate for simple types (eg value types and strings) + /// + public class SimplePropertyTypePredicateHandler : IPropertyPredicateHandler + { + /// + /// Returns true if the property type is a simple type (eg value types and strings) + /// + /// + /// + public bool CanHandle(PropertyInfo propertyInfo) + { + var propertyType = propertyInfo.PropertyType; + bool isSimpleType(Type x) => x.IsValueType || x == typeof(string); + return + isSimpleType(propertyType) || + propertyType.IsNullableOf(isSimpleType); + } + + /// + /// Returns null is the property is null or a predicate on + /// + /// + /// + /// + public Expression> Handle(TSource source, PropertyInfo propertyInfo) + { + var propertyValue = propertyInfo.GetValue(source); + if (propertyValue == null) + return null; + + var propertyType = propertyInfo.PropertyType; + var targetType = typeof(TTarget); + var targetPropery = targetType.GetProperty(propertyInfo.Name); // TODO: add support for attribute for setting target property name + if (targetPropery == null) + throw new Exception($"{targetType.Name} does not contain a property named {propertyInfo.Name}"); + + if (propertyType.IsNullable()) + propertyType = Nullable.GetUnderlyingType(propertyType); + + if (targetPropery.PropertyType != propertyType) + throw new Exception($"{targetPropery.Name} must be of type {propertyType.Name}"); + + var parameterExpression = Expression.Parameter(targetType, "x"); + var propertyExpression = Expression.Property(parameterExpression, targetPropery); + var valueExpression = Expression.Constant(propertyValue, propertyType); + var equalExpression = Expression.Equal(propertyExpression, valueExpression); + var result = Expression.Lambda>(equalExpression, parameterExpression); + return result; + } + } +}