mirror of https://github.com/sean-m/McRule.git
Added null literals via handlebar syntax.
parent
fac5734cf2
commit
9d35567c15
|
@ -1,4 +1,5 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
|
@ -6,7 +7,7 @@ using System.Reflection.Metadata;
|
|||
namespace McRule.Tests {
|
||||
public class Filtering {
|
||||
|
||||
People[] things = new[] {
|
||||
People[] peoples = new[] {
|
||||
new People("Sean", "Confused", 35, true, new[] {"muggle"}),
|
||||
new People("Sean", "Actor", 90, false, new[] {"muggle", "metallurgist"}),
|
||||
new People("Bean", "Runt", 20, false, new[] {"muggle", "giant"}),
|
||||
|
@ -15,6 +16,7 @@ namespace McRule.Tests {
|
|||
new People("Ragnar", "Viking", 25, true, new[] {"muggle", "grumpy"}),
|
||||
new People("Lars", "Viking", 30, false, new[] {"muggle", "grumpy"}),
|
||||
new People("Ferris", "Student", 17, true, new[] {"muggle"}),
|
||||
new People("Greta", null, 20, true, null),
|
||||
};
|
||||
|
||||
public class People
|
||||
|
@ -43,18 +45,37 @@ namespace McRule.Tests {
|
|||
stillWithUs == other.stillWithUs &&
|
||||
System.Linq.Enumerable.SequenceEqual(tags, other.tags);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return System.HashCode.Combine(name, kind, number, stillWithUs, tags);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"People({name}, {kind}, {number}, {stillWithUs}, {string.Join(", ", tags ?? new string[0])})";
|
||||
}
|
||||
}
|
||||
|
||||
#region testPolicies
|
||||
|
||||
ExpressionPolicy everyKindInclusive = new ExpressionPolicy {
|
||||
Name = "Any kind including null",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "*").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
ExpressionPolicy matchNullLiteral = new ExpressionPolicy {
|
||||
Name = "Any kind null",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "{{NULL}}").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
ExpressionPolicy matchNullByString = new ExpressionPolicy {
|
||||
Name = "Don't think this should work",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "null").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
ExpressionPolicy notSean = new ExpressionPolicy {
|
||||
Name = "Not named Sean",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
|
@ -121,13 +142,48 @@ namespace McRule.Tests {
|
|||
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.Or
|
||||
};
|
||||
|
||||
#endregion testPolicies
|
||||
|
||||
[SetUp]
|
||||
public void Setup() { }
|
||||
|
||||
[Test]
|
||||
public void MatchNullLiteral() {
|
||||
var filter = matchNullLiteral.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = peoples.Where(filter);
|
||||
|
||||
Assert.IsTrue(folks.All(x => x.kind == null));
|
||||
|
||||
// Test using EF generator
|
||||
var efGenerator = PredicateExpressionPolicyExtensions.GetEfExpressionGenerator();
|
||||
var efFilter = matchNullLiteral.GetPredicateExpression<People>(efGenerator)?.Compile();
|
||||
|
||||
folks = peoples.Where(efFilter);
|
||||
|
||||
Assert.IsTrue(folks.All(x => x.kind == null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MatchNullByString() {
|
||||
var expression = matchNullByString.GetPredicateExpression<People>();
|
||||
var filter = expression?.Compile();
|
||||
var folks = peoples.Where(filter);
|
||||
|
||||
Assert.Zero(folks.Count(), "A string value of null \"null\" should not evaluate to a null literal so should yield no results here.");
|
||||
|
||||
// Test using EF generator
|
||||
var efGenerator = PredicateExpressionPolicyExtensions.GetEfExpressionGenerator();
|
||||
var efFilter = matchNullByString.GetPredicateExpression<People>(efGenerator)?.Compile();
|
||||
|
||||
folks = peoples.Where(efFilter);
|
||||
|
||||
Assert.Zero(folks.Count(), "A string value of null \"null\" should not evaluate to a null literal so should yield no results here.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NegativeStringMatch() {
|
||||
var filter = notSean.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
var folks = peoples.Where(filter);
|
||||
|
||||
Assert.Null(folks.FirstOrDefault(x => x.name == "Sean"));
|
||||
}
|
||||
|
@ -137,7 +193,7 @@ namespace McRule.Tests {
|
|||
// Filter should match on people who's name ends in 'ean',
|
||||
// and case insensitive ends with 'EAN'.
|
||||
var filter = eans.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
var folks = peoples.Where(filter);
|
||||
|
||||
Assert.NotNull(folks);
|
||||
Assert.IsTrue(folks.All(x => x.name.EndsWith("ean")));
|
||||
|
@ -153,7 +209,7 @@ namespace McRule.Tests {
|
|||
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.And
|
||||
}?.GetPredicateExpression<People>()?.Compile();
|
||||
|
||||
var folks = things.Where(filter);
|
||||
var folks = peoples.Where(filter);
|
||||
|
||||
// Match should be exclusive enough to only include Ragnar
|
||||
Assert.NotNull(folks);
|
||||
|
@ -163,11 +219,11 @@ namespace McRule.Tests {
|
|||
// Process both expressions separately to verify they
|
||||
// have different results.
|
||||
filter = youngens.GetPredicateExpression<People>()?.Compile();
|
||||
folks = things.Where(filter);
|
||||
folks = peoples.Where(filter);
|
||||
Assert.IsTrue(folks.Count() > 1);
|
||||
|
||||
filter = vikings.GetPredicateExpression<People>()?.Compile();
|
||||
folks = things.Where(filter);
|
||||
folks = peoples.Where(filter);
|
||||
Assert.IsTrue(folks.Count() > 1);
|
||||
|
||||
// Original compound filter with an Or predicate
|
||||
|
@ -178,7 +234,7 @@ namespace McRule.Tests {
|
|||
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.Or
|
||||
}?.GetPredicateExpression<People>()?.Compile();
|
||||
|
||||
folks = things.Where(filter);
|
||||
folks = peoples.Where(filter);
|
||||
Assert.IsTrue(folks.Count() > 1);
|
||||
// Should include Vikings by kind and Student by number
|
||||
Assert.NotNull(folks.Where(x => x.kind == "Viking"));
|
||||
|
@ -188,7 +244,7 @@ namespace McRule.Tests {
|
|||
[Test]
|
||||
public void YoungPeople() {
|
||||
var filter = youngens.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
var folks = peoples.Where(filter);
|
||||
|
||||
Assert.NotNull(folks);
|
||||
Assert.IsTrue(folks.All(x => x.number >= 17 && x.number < 30));
|
||||
|
@ -197,7 +253,7 @@ namespace McRule.Tests {
|
|||
[Test]
|
||||
public void FilterListOfObjectsByMemberCollectionContents() {
|
||||
var filter = muggles.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
var folks = peoples.Where(filter);
|
||||
|
||||
Assert.NotNull(folks);
|
||||
Assert.IsTrue(folks.All(x => x.tags.Contains("muggle")));
|
||||
|
@ -206,7 +262,7 @@ namespace McRule.Tests {
|
|||
[Test]
|
||||
public void BoolConditional() {
|
||||
var filter = notQuiteDead.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
var folks = peoples.Where(filter);
|
||||
|
||||
Assert.NotNull(folks);
|
||||
Assert.IsTrue(folks.Count() > 0);
|
||||
|
@ -223,7 +279,7 @@ namespace McRule.Tests {
|
|||
[Test]
|
||||
public void TestPolicyWithOrConditional() {
|
||||
var filter = deadOrViking.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
var folks = peoples.Where(filter);
|
||||
|
||||
Assert.NotNull(folks);
|
||||
Assert.NotNull(folks.Where(x => x.kind == "Viking" && x.stillWithUs == false));
|
||||
|
@ -233,6 +289,14 @@ namespace McRule.Tests {
|
|||
Assert.Null(folks.FirstOrDefault(x => x.kind != "Viking" && x.stillWithUs == true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBooleanMatches() {
|
||||
var filter = notQuiteDead.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = peoples.Where(filter);
|
||||
|
||||
Assert.IsTrue(folks.All(x => x.stillWithUs == true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PolicyEFExpressionShouldNotEmitComparisonTypeStringMatches() {
|
||||
var filter = eans.GetPredicateExpression<People>();
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace McRule {
|
||||
public class LiteralValue {
|
||||
public dynamic Value { get; set; }
|
||||
public override string ToString() => "BaseLiteral";
|
||||
}
|
||||
|
||||
public class NullValue : LiteralValue {
|
||||
|
||||
public override string ToString() => "null";
|
||||
}
|
||||
}
|
|
@ -27,4 +27,8 @@
|
|||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Diagnostics.Contracts;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using static McRule.PredicateExpressionPolicyExtensions;
|
||||
|
||||
|
@ -23,7 +24,7 @@ public static partial class PredicateExpressionPolicyExtensions
|
|||
/// <summary>
|
||||
/// Prepend the given predicate with a short circuiting null check.
|
||||
/// </summary>
|
||||
internal static Expression AddNullCheck<T>(
|
||||
internal static Expression AddNotNullCheck<T>(
|
||||
Expression left,
|
||||
Expression expression)
|
||||
{
|
||||
|
@ -33,6 +34,16 @@ public static partial class PredicateExpressionPolicyExtensions
|
|||
return Expression.AndAlso(notNull, expression);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test for null value. This is used to test for null literals.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="expression"></param>
|
||||
/// <returns></returns>
|
||||
internal static Expression IsNull<T>(Expression expression) {
|
||||
return Expression.Equal(expression, Expression.Constant(null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies negative predicate to expression in a lambda.
|
||||
/// </summary>
|
||||
|
@ -92,7 +103,7 @@ public static partial class PredicateExpressionPolicyExtensions
|
|||
//LambdaExpression
|
||||
var containsCall = Expression.Call(arrContainsRuntimeMethod, opLeft, opRight);
|
||||
|
||||
var finalExpression = AddNullCheck<T>(opLeft, containsCall);
|
||||
var finalExpression = AddNotNullCheck<T>(opLeft, containsCall);
|
||||
|
||||
// Wrap it up in a warm lambda snuggie
|
||||
return Expression.Lambda<Func<T, bool>>(finalExpression, false, parameter);
|
||||
|
@ -184,6 +195,27 @@ public static partial class PredicateExpressionPolicyExtensions
|
|||
lambda.Parameters);
|
||||
}
|
||||
|
||||
static Regex handlebarPattern = new Regex(@"^(\{\{)(?<literal>.+)(\}\})", RegexOptions.ExplicitCapture
|
||||
| RegexOptions.Compiled);
|
||||
internal static (bool,LiteralValue?) GetStringValueLiteral(string value) {
|
||||
|
||||
/* Literal values are contained within handlebar syntax {{ literal }}
|
||||
* so values should be added to the switch statement below. Matching
|
||||
* cases should return early with their literal value.
|
||||
* */
|
||||
var matched = handlebarPattern.Match(value);
|
||||
if (matched.Success) {
|
||||
switch (matched.Groups.FirstOrDefault(x => x.Name == "literal")?.Value?.Trim()?.ToLower()) {
|
||||
case "null":
|
||||
return (true, new NullValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No literals found.
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dynamically build an expression suitable for filtering in a Where clause
|
||||
/// </summary>
|
||||
|
@ -191,6 +223,7 @@ public static partial class PredicateExpressionPolicyExtensions
|
|||
{
|
||||
var parameter = Expression.Parameter(typeof(T), "x");
|
||||
var opLeft = Expression.Property(parameter, property);
|
||||
(bool literalFound, LiteralValue? processedValue) = GetStringValueLiteral(value);
|
||||
var opRight = Expression.Constant(value);
|
||||
Expression? comparison = null;
|
||||
|
||||
|
@ -213,6 +246,11 @@ public static partial class PredicateExpressionPolicyExtensions
|
|||
hasComparable = lType.GetInterface("IComparable");
|
||||
}
|
||||
|
||||
if (literalFound) {
|
||||
if (processedValue is NullValue) {
|
||||
return Expression.Lambda<Func<T, bool>>(IsNull<T>(opLeft), parameter);
|
||||
}
|
||||
}
|
||||
|
||||
// For string comparisons using wildcards, trim the wildcard characters and pass to the comparison method
|
||||
if (lType == typeof(string))
|
||||
|
@ -264,9 +302,11 @@ public static partial class PredicateExpressionPolicyExtensions
|
|||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else if (hasCollection == typeof(ICollection)) {
|
||||
return GetArrayContainsExpression<T>(property, value);
|
||||
}
|
||||
else if (hasComparable == typeof(IComparable))
|
||||
{
|
||||
else if (hasComparable == typeof(IComparable)) {
|
||||
var operatorPrefix = Regex.Match(value.Trim(), @"^[!<>=]+");
|
||||
var operand = (operatorPrefix.Success ? value.Replace(operatorPrefix.Value, "") : value).Trim();
|
||||
|
||||
|
@ -285,10 +325,6 @@ public static partial class PredicateExpressionPolicyExtensions
|
|||
comparison = GetComparer(operatorPrefix.Value.Trim(), opLeftFinal, opRight);
|
||||
}
|
||||
}
|
||||
else if (hasCollection == typeof(ICollection))
|
||||
{
|
||||
return GetArrayContainsExpression<T>(property, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
comparison = Expression.Equal(opLeft, opRight);
|
||||
|
@ -300,7 +336,7 @@ public static partial class PredicateExpressionPolicyExtensions
|
|||
comparison = comparison == null ? falsePredicate : comparison;
|
||||
if (isNullable)
|
||||
{
|
||||
comparison = AddNullCheck<T>(opLeft, comparison);
|
||||
comparison = AddNotNullCheck<T>(opLeft, comparison);
|
||||
}
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(comparison ?? Expression.Equal(opLeft, opRight), parameter);
|
||||
|
@ -373,6 +409,7 @@ public abstract class ExpressionGeneratorBase : ExpressionGenerator
|
|||
{
|
||||
var parameter = Expression.Parameter(typeof(T), "x");
|
||||
var opLeft = Expression.Property(parameter, property);
|
||||
(bool literalFound, LiteralValue? processedValue) = GetStringValueLiteral(value);
|
||||
var opRight = Expression.Constant(value);
|
||||
Expression? comparison = null;
|
||||
|
||||
|
@ -385,8 +422,7 @@ public abstract class ExpressionGeneratorBase : ExpressionGenerator
|
|||
var isNullable = false;
|
||||
Type? hasComparable = lType.GetInterface("IComparable");
|
||||
Type? hasCollection = lType.GetInterface("ICollection");
|
||||
if (hasComparable == null && opLeft.Type.IsValueType)
|
||||
{
|
||||
if (hasComparable == null && opLeft.Type.IsValueType) {
|
||||
lType = Nullable.GetUnderlyingType(opLeft.Type);
|
||||
// Nullable.GetUnderlyingType only returns a non-null value if the
|
||||
// supplied type was indeed a nullable type.
|
||||
|
@ -395,6 +431,12 @@ public abstract class ExpressionGeneratorBase : ExpressionGenerator
|
|||
hasComparable = lType.GetInterface("IComparable");
|
||||
}
|
||||
|
||||
if (literalFound) {
|
||||
if (processedValue == null) {
|
||||
return Expression.Lambda<Func<T, bool>>(IsNull<T>(opLeft), parameter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// For string comparisons using wildcards, trim the wildcard characters and pass to the comparison method
|
||||
if (lType == typeof(string))
|
||||
|
@ -482,7 +524,7 @@ public abstract class ExpressionGeneratorBase : ExpressionGenerator
|
|||
comparison = comparison == null ? falsePredicate : comparison;
|
||||
if (isNullable)
|
||||
{
|
||||
comparison = AddNullCheck<T>(opLeft, comparison);
|
||||
comparison = AddNotNullCheck<T>(opLeft, comparison);
|
||||
}
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(comparison ?? Expression.Equal(opLeft, opRight), parameter);
|
||||
|
|
14
README.md
14
README.md
|
@ -19,6 +19,20 @@ A simple equality comparison is used by default but operators can be prefixed to
|
|||
|
||||
> Note: the IComparable interface is mostly used for numerical types but custom types with comparison providers may work at runtime.
|
||||
|
||||
### Literal Values
|
||||
Literal values, as needed, use handlbar syntax: {{ value }}. Null checks are implicitly added to most expressions but sometimes you need an expression that evaluates true for null values. In that case, a null literal is represented as {{null}}.
|
||||
Case sensitivity doesn't matter, nor does internal whitespace inside the braces. Values are interpretted like so:
|
||||
```csharp
|
||||
var matched = handlebarPattern.Match(value);
|
||||
if (matched.Success) {
|
||||
switch (matched.Groups.FirstOrDefault(x => x.Name == "literal")?.Value?.Trim()?.ToLower()) {
|
||||
case "null":
|
||||
return (true, new NullValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue