Skip to content

Commit

Permalink
Adds PredicateGenerator (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
Martin Oom authored May 6, 2019
1 parent 574835c commit 015c0ed
Show file tree
Hide file tree
Showing 14 changed files with 1,041 additions and 0 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,48 @@ Example:
Expression<Func<string, bool>> stringExpression = a => a == "123";
var myClassExpression = stringExpression.RemapTo<MyClass, string, bool>(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<string> Category { get; set; }
}

public MyApiController : BaseController
{
private readonly IQueryable<MyEntity> _entities;

// WebApi action method
public IEnumerable<MyEntity> Get(MyEntityFilter filter)
{
var predicate = new PredicateBuilder<MyEntityFilter, MyEntity>().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
Original file line number Diff line number Diff line change
@@ -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<string>("json"), this, settings);
}

public void Serialize(IXunitSerializationInfo info)
{
info.AddValue("json", JsonConvert.SerializeObject(this, settings));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Scenario> GetScenarios()
{
return new TheoryData<Scenario>
{
new Scenario
{
Enumerable = Array.Empty<bool>(),
ExpectedResult = true
},
new Scenario
{
Enumerable = Array.Empty<string>(),
ExpectedResult = true
},
new Scenario
{
Enumerable = new Collection<decimal>(),
ExpectedResult = true
},
new Scenario
{
Enumerable = new List<int>(),
ExpectedResult = true
},
new Scenario
{
Enumerable = new HashSet<int>(),
ExpectedResult = true
},
new Scenario
{
Enumerable = new bool[] { false },
ExpectedResult = false
},
new Scenario
{
Enumerable = new string[] { "" },
ExpectedResult = false
},
new Scenario
{
Enumerable = new Collection<decimal>{ 1M },
ExpectedResult = false
},
new Scenario
{
Enumerable = new List<int>{ 0, 1, 2 },
ExpectedResult = false
},
new Scenario
{
Enumerable = new HashSet<int>{ 123 },
ExpectedResult = false
}
};
}

public class Scenario : XunitSerializable
{
public IEnumerable Enumerable { get; set; }
public bool ExpectedResult { get; set; }
}
}
}
144 changes: 144 additions & 0 deletions src/Spinit.Expressions.UnitTests/Internals/TypeExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -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<int>).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<string>).IsNullable());
}
#endif
public class TestClass
{
public int IntProperty1 { get; set; }
}
}

public class IsNullableOfT
{
[Fact]
public void NullableIntIsNotNullableOfBool()
{
Assert.False(typeof(int?).IsNullableOf<bool>());
}

[Fact]
public void NullableIntIsNullableOfInt()
{
Assert.True(typeof(int?).IsNullableOf<int>());
}
}

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<string>).IsEnumerable());
}

[Fact]
public void ListOfStringIsAnEnumerable()
{
Assert.True(typeof(List<string>).IsEnumerable());
}

[Fact]
public void BoolArrayIsAnEnumerable()
{
Assert.True(typeof(bool[]).IsEnumerable());
}
}

public class IsEnumerableOfT
{
[Fact]
public void StringIsAnEnumerableOfChar()
{
Assert.True(typeof(string).IsEnumerableOf<char>());
}

[Fact]
public void ICollectionOfStringIsAnEnumerableOfString()
{
Assert.True(typeof(ICollection<string>).IsEnumerableOf<string>());
}

[Fact]
public void ListOfStringIsAnEnumerableOfString()
{
Assert.True(typeof(List<string>).IsEnumerableOf<string>());
}
}

public class IsEnumerableOf
{
[Fact]
public void IntArrayIsEnumerableOfPrimitiveType()
{
Assert.True(typeof(int[]).IsEnumerableOf(x => x.IsPrimitive));
}

[Fact]
public void StringListIsEnumerableOfString()
{
Assert.True(typeof(List<string>).IsEnumerableOf(x => x == typeof(string)));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TodoFilter, Todo>();
var predicate = predicateGenerator.Generate(filter);
//var expectedPredicate = ExpressionExtensions.Expression(x => x.Id == 123);
Expression<Func<Todo, bool>> 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<TodoFilter, Todo>();
var predicate = predicateGenerator.Generate(filter);

var items = new List<Todo>
{
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<TodoFilter, Todo>();
// var predicate = predicateGenerator.Generate(filter);

// var items = new List<Todo>
// {
// 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<string> Type { get; set; }

// TODO: add OperatorAttribute (AND | OR) + implement IEnumerable support on target side
//public IEnumerable<string> Tags { get; set; }
}

public class Todo
{
public int Id { get; set; }
public string Type { get; set; }
//public IEnumerable<string> Tags { get; set; }
}
}
}
Loading

0 comments on commit 015c0ed

Please sign in to comment.