Added allowance for lvalue comparison operators to so you can filter for properties > < >= <= <> a given value. Also added handling for Nullable types.

pull/2/head
Sean McArde 2023-03-13 18:01:54 -07:00
parent 797350384d
commit ea36a8ff15
2 changed files with 160 additions and 91 deletions

View File

@ -1,5 +1,6 @@

using System.Linq.Expressions;
using System.Text.RegularExpressions;
namespace Ruler;
@ -13,124 +14,192 @@ public class FilterPolicy
}
public static class FilterPolicyExtensions
{
public enum RuleOperator
{
And,
Or
}
{
public enum RuleOperator
{
And,
Or
}
public static Expression<Func<T, bool>> AddFilterToStringProperty<T>(
Expression<Func<T, string>> expression, string filter, string filterType)
{
/// <summary>
/// Builds expressions using string member functions StartsWith, EndsWith or Contains as the comparator.
/// </summary>
public static Expression<Func<T, bool>> AddFilterToStringProperty<T>(
Expression<Func<T, string>> expression, string filter, string filterType)
{
#if DEBUG
if (!(filterType == "StartsWith" || filterType == "EndsWith" || filterType == "Contains"))
{
throw new Exception($"filterType must equal StartsWith, EndsWith or Contains. Passed {filterType}");
}
#if DEBUG
if (!(filterType == "StartsWith" || filterType == "EndsWith" || filterType == "Contains"))
{
throw new Exception($"filterType must equal StartsWith, EndsWith or Contains. Passed {filterType}");
}
#endif
// Check that the property isn't null, otherwise we'd hit null object exceptions at runtime
var notNull = Expression.NotEqual(expression.Body, Expression.Constant(null));
var notNull = Expression.NotEqual(expression.Body, Expression.Constant(null));
// Setup calls to EtartsWith, EndsWith, or Contains
// TODO expressionArgs was used to pass multiple values for case insensitive compare, wasn't
// mapping the method correctly when used with EF so need to revisit that
var expressionArgs = new Expression[] { Expression.Constant(filter) };
var strPredicate = Expression.Call(expression.Body, filterType, null, expressionArgs);
var expressionArgs = new Expression[] { Expression.Constant(filter) };
var strPredicate = Expression.Call(expression.Body, filterType, null, expressionArgs);
var filterExpression = Expression.AndAlso(notNull, strPredicate);
var filterExpression = Expression.AndAlso(notNull, strPredicate);
return Expression.Lambda<Func<T, bool>>(
filterExpression,
expression.Parameters);
}
return Expression.Lambda<Func<T, bool>>(
filterExpression,
expression.Parameters);
}
public static Expression AddNullCheck<T>(
Expression left,
Expression expression)
{
// Check that the property isn't null, otherwise we'd hit null object exceptions at runtime
var notNull = Expression.NotEqual(left, Expression.Constant(null));
return Expression.AndAlso(notNull, expression);
}
private static Expression GetComparer(string op, Expression left, Expression right) => op switch
{
">" => Expression.GreaterThan(left, right),
">=" => Expression.GreaterThanOrEqual(left, right),
"<" => Expression.LessThan(left, right),
"<=" => Expression.LessThanOrEqual(left, right),
"<>" => Expression.NotEqual(left, right),
"!=" => Expression.NotEqual(left, right),
"!" => Expression.NotEqual(left, right),
_ => Expression.Equal(left, right)
};
private static Type[] intTypes = new Type[]{ typeof(Int16), typeof(Int32), typeof(Int64),
typeof(UInt16), typeof(UInt32), typeof(UInt64),
typeof(Int16?), typeof(Int32?), typeof(Int64?),
typeof(UInt16?), typeof(UInt32?), typeof(UInt64?)};
// Dynamically build an expression suitable for filtering in a Where clause
public static Expression<Func<T, bool>> GetFilterForType<T>(string property, string value)
{
var parameter = Expression.Parameter(typeof(T), "x");
var opLeft = Expression.Property(parameter, property);
var opRight = Expression.Constant(value);
var comparison = Expression.Equal(opLeft, opRight);
public static Expression<Func<T, bool>> GetFilterExpressionForType<T>(string property, string value)
{
var parameter = Expression.Parameter(typeof(T), "x");
var opLeft = Expression.Property(parameter, property);
var opRight = Expression.Constant(value);
Expression? comparison = null;
// For string comparisons using wildcards, trim the wildcard characters and pass to the comparison method
if (opLeft.Type == typeof(string))
{
if (opLeft.Type == typeof(string)) {
// Grab the object property for use in the inner expression body
var strParam = Expression.Lambda<Func<T, string>>(opLeft, parameter);
var strParam = Expression.Lambda<Func<T,string>>(opLeft, parameter);
if (value.StartsWith("*") && value.EndsWith("*")) {
return AddFilterToStringProperty<T>(strParam, value.Trim('*'), "Contains");
} else if (value.StartsWith("*")) {
return AddFilterToStringProperty<T>(strParam, value.TrimStart('*'), "EndsWith");
} else if (value.EndsWith("*")) {
return AddFilterToStringProperty<T>(strParam, value.TrimEnd('*'), "StartsWith");
} else {
comparison = Expression.Equal(opLeft, opRight);
return Expression.Lambda<Func<T, bool>>(comparison, parameter);
}
}
if (value.StartsWith("*") && value.EndsWith("*"))
{
return AddFilterToStringProperty<T>(strParam, value.Trim('*'), "Contains");
}
else if (value.StartsWith("*"))
{
return AddFilterToStringProperty<T>(strParam, value.TrimStart('*'), "EndsWith");
}
else if (value.EndsWith("*"))
{
return AddFilterToStringProperty<T>(strParam, value.TrimEnd('*'), "StartsWith");
}
else
{
return Expression.Lambda<Func<T, bool>>(comparison, parameter);
}
}
// For IComparable types on the left hand side, attempt to parse the right hand side
// into the same type and use <,>,<=,>=,= prefixes to infer BinaryExpression type.
// Should work with numerical or datetime values provided they parse correctly.
// Note, a float on the right hand side will not parse into an integer type implicitly
// so it is parsed into a decimal value first and then rounded to the nearest integral.
var lType = opLeft.Type;
var isNullable = false;
Type? interfaceType = lType.GetInterface("IComparable");
if (interfaceType == 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.
if (lType != null)
isNullable = true;
interfaceType = lType.GetInterface("IComparable");
}
if (interfaceType == typeof(IComparable))
{
var operatorPrefix = Regex.Match(value.Trim(), @"^[!<>=]+");
var operand = (operatorPrefix.Success ? value.Replace(operatorPrefix.Value, "") : value).Trim();
if (!String.IsNullOrEmpty(operand))
{
var parseMethod = lType.GetMethods().FirstOrDefault(x => x.Name == "Parse");
if (intTypes.Contains(opLeft.Type)) {
operand = operand.Contains(".") ? Math.Round(decimal.Parse(operand)).ToString() : operand;
}
var opRightNumerical = parseMethod?.Invoke(null, new string[] { operand });
return Expression.Lambda<Func<T, bool>>(comparison, parameter);
}
opRight = Expression.Constant(opRightNumerical);
comparison = GetComparer(operatorPrefix.Value.Trim(), Expression.Convert(opLeft, lType), opRight);
}
} else {
comparison = Expression.Equal(opLeft, opRight);
}
// If comparison is null that means we haven't been able to infer a good comparison
// expression for it so just defer to a false literal.
Expression<Func<T, bool>> falsePredicate = x => false;
comparison = comparison == null ? falsePredicate : comparison;
comparison = (isNullable) ? AddNullCheck<T>(opLeft, comparison) : comparison;
return Expression.Lambda<Func<T, bool>>(comparison ?? Expression.Equal(opLeft, opRight), parameter);
}
// Combine a list of expressions inclusively
public static Expression<Func<T, bool>>? CombineAnd<T>(IEnumerable<Expression<Func<T, bool>>> predicates)
{
if (predicates.Count() == 0) return null;
public static Expression<Func<T, bool>>? CombineAnd<T>(IEnumerable<Expression<Func<T, bool>>> predicates)
{
if (predicates.Count() == 0) return null;
var final = predicates.First();
foreach (var next in predicates.Skip(1))
final = PredicateBuilder.And(final, next);
return final;
}
var final = predicates.First();
foreach (var next in predicates.Skip(1))
final = PredicateBuilder.And(final, next);
return final;
}
// Combine a list of expressions inclusively
public static Expression<Func<T, bool>>? CombineOr<T>(IEnumerable<Expression<Func<T, bool>>> predicates)
{
if (predicates.Count() == 0) return null;
public static Expression<Func<T, bool>>? CombineOr<T>(IEnumerable<Expression<Func<T, bool>>> predicates)
{
if (predicates.Count() == 0) return null;
var final = predicates.First();
foreach (var next in predicates.Skip(1))
final = PredicateBuilder.Or(final, next);
var final = predicates.First();
foreach (var next in predicates.Skip(1))
final = PredicateBuilder.Or(final, next);
return final;
}
return final;
}
// Combine a list of expressions inclusively
public static Expression<Func<T, bool>>? CombinePredicates<T>(IEnumerable<Expression<Func<T, bool>>> predicates,
FilterPolicyExtensions.RuleOperator op)
{
if (predicates.Count() == 0) return null;
public static Expression<Func<T, bool>>? CombinePredicates<T>(IEnumerable<Expression<Func<T, bool>>> predicates, FilterPolicyExtensions.RuleOperator op)
{
if (predicates.Count() == 0) return null;
if (op == RuleOperator.And)
{
return CombineAnd(predicates);
}
return CombineOr(predicates);
}
if (op == RuleOperator.And)
{
return CombineAnd(predicates);
}
return CombineOr(predicates);
}
public static Expression<Func<T, bool>>? GetFilterExpression<T>(this FilterPolicy policy)
{
var predicates = new List<Expression<Func<T, bool>>>();
foreach (var constraints in policy.scope)
{
predicates.Add(GetFilterForType<T>(constraints.Item1, constraints.Item2));
}
return CombinePredicates<T>(predicates, policy.ruleOperator);
}
}
public static Expression<Func<T, bool>> GetFilterExpression<T>(this FilterPolicy policy)
{
var predicates = new List<Expression<Func<T, bool>>>();
foreach (var constraints in policy.scope)
{
predicates.Add(GetFilterExpressionForType<T>(constraints.Item1, constraints.Item2));
}
return CombinePredicates<T>(predicates, policy.ruleOperator);
}
}

View File

@ -50,10 +50,10 @@ users.Where(filterExpression).Dump($"operator {filterPolicy.ruleOperator}");
var dynamicFilterDAS = FilterPolicyExtensions.GetFilterForType<User>("agency", "DAS");
var dynamicFilterDAS = FilterPolicyExtensions.GetFilterExpressionForType<User>("agency", "DAS");
users.Where(dynamicFilterDAS).Dump("DAS users");
var dynamicFilterBrian = FilterPolicyExtensions.GetFilterForType<User>("first","Brian");
var dynamicFilterBrian = FilterPolicyExtensions.GetFilterExpressionForType<User>("first","Brian");
var policies = new List<Expression<Func<User,bool>>>();
policies.Add(dynamicFilterDAS);