mirror of https://github.com/sean-m/McRule.git
Needs refactoring of duplicated code. Tests pass.
parent
99dcf3f3df
commit
2446257d6d
|
@ -19,11 +19,11 @@ namespace McRule.Tests {
|
|||
|
||||
public class People
|
||||
{
|
||||
public readonly string name;
|
||||
public readonly string kind;
|
||||
public readonly int? number;
|
||||
public readonly bool stillWithUs;
|
||||
public readonly string[] tags;
|
||||
public string name { get; set; }
|
||||
public string kind { get; set; }
|
||||
public int? number { get; set; }
|
||||
public bool stillWithUs { get; set; }
|
||||
public string[] tags { get; set; }
|
||||
|
||||
public People(string name, string kind, int? number, bool stillWithUs, string[] tags = null)
|
||||
{
|
||||
|
@ -122,13 +122,11 @@ namespace McRule.Tests {
|
|||
};
|
||||
|
||||
[SetUp]
|
||||
public void Setup() {
|
||||
PredicateExpressionPolicyExtensions.Init();
|
||||
}
|
||||
public void Setup() { }
|
||||
|
||||
[Test]
|
||||
public void NegativeStringMatch() {
|
||||
var filter = notSean.GetExpression<People>()?.Compile();
|
||||
var filter = notSean.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
|
||||
Assert.Null(folks.FirstOrDefault(x => x.name == "Sean"));
|
||||
|
@ -138,7 +136,7 @@ namespace McRule.Tests {
|
|||
public void EndsWith() {
|
||||
// Filter should match on people who's name ends in 'ean',
|
||||
// and case insensitive ends with 'EAN'.
|
||||
var filter = eans.GetExpression<People>()?.Compile();
|
||||
var filter = eans.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
|
||||
Assert.NotNull(folks);
|
||||
|
@ -153,7 +151,7 @@ namespace McRule.Tests {
|
|||
youngens, vikings
|
||||
},
|
||||
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.And
|
||||
}?.GetExpression<People>()?.Compile();
|
||||
}?.GetPredicateExpression<People>()?.Compile();
|
||||
|
||||
var folks = things.Where(filter);
|
||||
|
||||
|
@ -164,11 +162,11 @@ namespace McRule.Tests {
|
|||
|
||||
// Process both expressions separately to verify they
|
||||
// have different results.
|
||||
filter = youngens.GetExpression<People>()?.Compile();
|
||||
filter = youngens.GetPredicateExpression<People>()?.Compile();
|
||||
folks = things.Where(filter);
|
||||
Assert.IsTrue(folks.Count() > 1);
|
||||
|
||||
filter = vikings.GetExpression<People>()?.Compile();
|
||||
filter = vikings.GetPredicateExpression<People>()?.Compile();
|
||||
folks = things.Where(filter);
|
||||
Assert.IsTrue(folks.Count() > 1);
|
||||
|
||||
|
@ -178,7 +176,7 @@ namespace McRule.Tests {
|
|||
youngens, vikings
|
||||
},
|
||||
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.Or
|
||||
}?.GetExpression<People>()?.Compile();
|
||||
}?.GetPredicateExpression<People>()?.Compile();
|
||||
|
||||
folks = things.Where(filter);
|
||||
Assert.IsTrue(folks.Count() > 1);
|
||||
|
@ -198,7 +196,7 @@ namespace McRule.Tests {
|
|||
|
||||
[Test]
|
||||
public void FilterListOfObjectsByMemberCollectionContents() {
|
||||
var filter = muggles.GetExpression<People>()?.Compile();
|
||||
var filter = muggles.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
|
||||
Assert.NotNull(folks);
|
||||
|
@ -207,7 +205,7 @@ namespace McRule.Tests {
|
|||
|
||||
[Test]
|
||||
public void BoolConditional() {
|
||||
var filter = notQuiteDead.GetExpression<People>()?.Compile();
|
||||
var filter = notQuiteDead.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
|
||||
Assert.NotNull(folks);
|
||||
|
@ -217,14 +215,14 @@ namespace McRule.Tests {
|
|||
[Test]
|
||||
public void NullFilterWhenNoMatchingTypes() {
|
||||
// Shouldn't have any filters in the policy for string objects.
|
||||
var filter = notQuiteDead.GetExpression<string>()?.Compile();
|
||||
var filter = notQuiteDead.GetPredicateExpression<string>()?.Compile();
|
||||
|
||||
Assert.Null(filter);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPolicyWithOrConditional() {
|
||||
var filter = deadOrViking.GetExpression<People>()?.Compile();
|
||||
var filter = deadOrViking.GetPredicateExpression<People>()?.Compile();
|
||||
var folks = things.Where(filter);
|
||||
|
||||
Assert.NotNull(folks);
|
||||
|
@ -238,7 +236,9 @@ namespace McRule.Tests {
|
|||
[Test]
|
||||
public void PolicyEFExpressionShouldNotEmitComparisonTypeStringMatches() {
|
||||
var filter = eans.GetPredicateExpression<People>();
|
||||
var efFilter = eans.GetEFPredicateExpression<People>();
|
||||
|
||||
var efGenerator = PredicateExpressionPolicyExtensions.GetEfExpressionGenerator();
|
||||
var efFilter = eans.GetPredicateExpression<People>(efGenerator);
|
||||
|
||||
Assert.NotNull(filter);
|
||||
Assert.NotNull(efFilter.ToString(), filter.ToString());
|
||||
|
|
|
@ -6,12 +6,14 @@ using System.Text;
|
|||
namespace McRule {
|
||||
public interface IExpressionRule {
|
||||
string TargetType { get; set; }
|
||||
Expression<Func<T, bool>>? GetExpression<T>();
|
||||
Expression<Func<T, bool>>? GetExpression<T>(ExpressionOptions options);
|
||||
Expression<Func<T, bool>>? GetPredicateExpression<T>();
|
||||
Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionGenerator generator);
|
||||
}
|
||||
|
||||
public interface ExpressionOptions {
|
||||
public bool SupportEF { get; }
|
||||
public bool NoCache { get; }
|
||||
public interface ExpressionGenerator
|
||||
{
|
||||
Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionRuleCollection policy);
|
||||
|
||||
Expression<Func<T, bool>> GetPredicateExpressionForType<T>(string property, string value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,20 +13,18 @@ namespace McRule {
|
|||
|
||||
public ExpressionRuleCollection() { }
|
||||
|
||||
public Expression<Func<T, bool>>? GetExpression<T>() {
|
||||
var expressions = Rules.Select(x => x.GetExpression<T>());
|
||||
public Expression<Func<T, bool>>? GetPredicateExpression<T>() {
|
||||
var expressions = Rules.Select(x => x.GetPredicateExpression<T>());
|
||||
|
||||
return (RuleOperator == PredicateExpressionPolicyExtensions.RuleOperator.Or)
|
||||
? PredicateExpressionPolicyExtensions.CombineOr<T>(expressions)
|
||||
: PredicateExpressionPolicyExtensions.CombineAnd<T>(expressions);
|
||||
}
|
||||
|
||||
public Expression<Func<T, bool>>? GetExpression<T>(ExpressionOptions options) {
|
||||
var expressions = Rules.Select(x => x.GetExpression<T>(options));
|
||||
|
||||
return (RuleOperator == PredicateExpressionPolicyExtensions.RuleOperator.Or)
|
||||
? PredicateExpressionPolicyExtensions.CombineOr<T>(expressions)
|
||||
: PredicateExpressionPolicyExtensions.CombineAnd<T>(expressions);
|
||||
public Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionGenerator generator)
|
||||
{
|
||||
var expressions = generator.GetPredicateExpression<T>(this);
|
||||
return expressions;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +54,7 @@ namespace McRule {
|
|||
/// <summary>
|
||||
/// Returns an expression tree targeting an object type based on policy parameters.
|
||||
/// </summary>
|
||||
public Expression<Func<T, bool>>? GetExpression<T>() {
|
||||
public Expression<Func<T, bool>>? GetPredicateExpression<T>() {
|
||||
if (!(typeof(T).Name.Equals(this.TargetType, StringComparison.CurrentCultureIgnoreCase))) return null;
|
||||
|
||||
if (cachedExpression == null) {
|
||||
|
@ -69,14 +67,10 @@ namespace McRule {
|
|||
/// <summary>
|
||||
/// Returns an expression tree targeting an object type based on policy parameters.
|
||||
/// </summary>
|
||||
public Expression<Func<T, bool>>? GetExpression<T>(ExpressionOptions options) {
|
||||
public Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionGenerator generator) {
|
||||
if (!(typeof(T).Name.Equals(this.TargetType, StringComparison.CurrentCultureIgnoreCase))) return null;
|
||||
|
||||
if (cachedExpression == null && !options.NoCache) {
|
||||
cachedExpression = PredicateExpressionPolicyExtensions.GetPredicateExpressionForType<T>(this.Property, this.Value);
|
||||
}
|
||||
|
||||
return PredicateExpressionPolicyExtensions.GetPredicateExpressionForType<T>(this.Property, this.Value);
|
||||
return generator.GetPredicateExpressionForType<T>(this.Property, this.Value);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
|
@ -84,7 +78,7 @@ namespace McRule {
|
|||
}
|
||||
|
||||
public string GetFilterString<T>() {
|
||||
return this.GetExpression<T>()?.ToString() ?? String.Empty;
|
||||
return this.GetPredicateExpression<T>()?.ToString() ?? String.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,26 +8,26 @@ using static McRule.PredicateExpressionPolicyExtensions;
|
|||
|
||||
namespace McRule;
|
||||
|
||||
public static partial class PredicateExpressionPolicyExtensions {
|
||||
public enum RuleOperator {
|
||||
public static partial class PredicateExpressionPolicyExtensions
|
||||
{
|
||||
public enum RuleOperator
|
||||
{
|
||||
And,
|
||||
Or
|
||||
}
|
||||
|
||||
public static ExpressionRule ToFilterRule(this (string, string, string) tuple) {
|
||||
public static ExpressionRule ToFilterRule(this (string, string, string) tuple)
|
||||
{
|
||||
return new ExpressionRule(tuple);
|
||||
}
|
||||
|
||||
internal delegate Expression<Func<T, bool>> AddStringPropertyExpression<T>(
|
||||
Expression<Func<T, string>> lambda, string filter, string filterType, bool ignoreCase = false);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Prepend the given predicate with a short circuiting null check.
|
||||
/// </summary>
|
||||
internal static Expression AddNullCheck<T>(
|
||||
Expression left,
|
||||
Expression expression) {
|
||||
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));
|
||||
|
||||
|
@ -40,7 +40,8 @@ public static partial class PredicateExpressionPolicyExtensions {
|
|||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="operand"></param>
|
||||
/// <returns></returns>
|
||||
internal static Expression<Func<T, bool>> Negate<T>(Expression<Func<T, bool>> lambda) {
|
||||
internal static Expression<Func<T, bool>> Negate<T>(Expression<Func<T, bool>> lambda)
|
||||
{
|
||||
var body = lambda.Body;
|
||||
var parameters = lambda.Parameters;
|
||||
|
||||
|
@ -54,7 +55,8 @@ public static partial class PredicateExpressionPolicyExtensions {
|
|||
/// Return a binary expression based on the given filter string. Default to a
|
||||
/// standard Equals comparison.
|
||||
/// </summary>
|
||||
private static Expression GetComparer(string op, Expression left, Expression right) => op switch {
|
||||
internal static Expression GetComparer(string op, Expression left, Expression right) => op switch
|
||||
{
|
||||
">" => Expression.GreaterThan(left, right),
|
||||
">=" => Expression.GreaterThanOrEqual(left, right),
|
||||
"<" => Expression.LessThan(left, right),
|
||||
|
@ -66,115 +68,16 @@ public static partial class PredicateExpressionPolicyExtensions {
|
|||
};
|
||||
|
||||
// Used to test for numerical integer types when casting a float to integer.
|
||||
private static Type[] intTypes = { typeof(Int16), typeof(Int32), typeof(Int64),
|
||||
typeof(UInt16), typeof(UInt32), typeof(UInt64),
|
||||
typeof(Int16?), typeof(Int32?), typeof(Int64?),
|
||||
typeof(UInt16?), typeof(UInt32?), typeof(UInt64?)};
|
||||
internal static Type[] intTypes =
|
||||
{
|
||||
typeof(Int16), typeof(Int32), typeof(Int64),
|
||||
typeof(UInt16), typeof(UInt32), typeof(UInt64),
|
||||
typeof(Int16?), typeof(Int32?), typeof(Int64?),
|
||||
typeof(UInt16?), typeof(UInt32?), typeof(UInt64?)
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Dynamically build an expression suitable for filtering in a Where clause
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>> GetPredicateExpressionForType<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 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? hasComparable = lType.GetInterface("IComparable");
|
||||
Type? hasCollection = lType.GetInterface("ICollection");
|
||||
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.
|
||||
if (lType != null)
|
||||
isNullable = true;
|
||||
hasComparable = lType.GetInterface("IComparable");
|
||||
}
|
||||
|
||||
|
||||
// For string comparisons using wildcards, trim the wildcard characters and pass to the comparison method
|
||||
if (lType == typeof(string))
|
||||
{
|
||||
Expression<Func<T, bool>> result;
|
||||
|
||||
// Grab the object property for use in the inner expression body
|
||||
var strParam = Expression.Lambda<Func<T, string>>(opLeft, parameter);
|
||||
|
||||
// If a string match begins with !, we negate the result.
|
||||
var negateResult = false;
|
||||
if (value.StartsWith("!")) {
|
||||
negateResult = true;
|
||||
value = value.TrimStart('!');
|
||||
}
|
||||
|
||||
// String comparisons which are prefixed with '~' will be evaluated ignoring case.
|
||||
// Note: when expression trees are used outside .net, such as with EF to SQL Server,
|
||||
// default case sensitivity for that environment may apply implicitly and counter to
|
||||
// filter policy intent.
|
||||
bool ignoreCase = false;
|
||||
if (value.StartsWith('~')) {
|
||||
ignoreCase = true;
|
||||
value = value.TrimStart('~');
|
||||
}
|
||||
|
||||
if (value.StartsWith("*") && value.EndsWith("*")) {
|
||||
result = funcs.AddStringPropertyExpression<T>(strParam, value.Trim('*'), "Contains", ignoreCase);
|
||||
} else if (value.StartsWith("*")) {
|
||||
result = funcs.AddStringPropertyExpression<T>(strParam, value.TrimStart('*'), "EndsWith", ignoreCase);
|
||||
} else if (value.EndsWith("*")) {
|
||||
result = funcs.AddStringPropertyExpression<T>(strParam, value.TrimEnd('*'), "StartsWith", ignoreCase);
|
||||
} else {
|
||||
result = funcs.AddStringPropertyExpression<T>(strParam, value, "Equals", ignoreCase);
|
||||
}
|
||||
|
||||
if (negateResult) {
|
||||
result = Negate<T>(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else if (hasComparable == 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 });
|
||||
|
||||
opRight = Expression.Constant(opRightNumerical);
|
||||
Expression opLeftFinal = isNullable ? Expression.Convert(opLeft, lType) : opLeft;
|
||||
comparison = GetComparer(operatorPrefix.Value.Trim(), opLeftFinal, opRight);
|
||||
}
|
||||
} else if (hasCollection == typeof(ICollection)) {
|
||||
return GetArrayContainsExpression<T>(property, value);
|
||||
} 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;
|
||||
if (isNullable) {
|
||||
comparison = AddNullCheck<T>(opLeft, comparison);
|
||||
}
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(comparison ?? Expression.Equal(opLeft, opRight), parameter);
|
||||
}
|
||||
|
||||
static Expression<Func<T, bool>> GetArrayContainsExpression<T>(string property, object value) {
|
||||
internal static Expression<Func<T, bool>> GetArrayContainsExpression<T>(string property, object value)
|
||||
{
|
||||
// Bind to the property by name and make the constant value
|
||||
// we'll be passing into the Contains() call
|
||||
var parameter = Expression.Parameter(typeof(T), "x");
|
||||
|
@ -200,7 +103,8 @@ public static partial class PredicateExpressionPolicyExtensions {
|
|||
/// Combine a list of expressions exclusively with AndAlso predicate from
|
||||
/// PredicateBuilder. This operator short circuits.
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>>? CombineAnd<T>(IEnumerable<Expression<Func<T, bool>>> predicates) {
|
||||
public static Expression<Func<T, bool>>? CombineAnd<T>(IEnumerable<Expression<Func<T, bool>>> predicates)
|
||||
{
|
||||
if (predicates.Count() == 0) return null;
|
||||
|
||||
var final = predicates.First();
|
||||
|
@ -214,7 +118,8 @@ public static partial class PredicateExpressionPolicyExtensions {
|
|||
/// Combine a list of expressions inclusively with an Or predicate
|
||||
/// from PredicateBuilder.
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>>? CombineOr<T>(IEnumerable<Expression<Func<T, bool>>> predicates) {
|
||||
public static Expression<Func<T, bool>>? CombineOr<T>(IEnumerable<Expression<Func<T, bool>>> predicates)
|
||||
{
|
||||
if (predicates.Count() == 0) return null;
|
||||
|
||||
var final = predicates.First();
|
||||
|
@ -227,24 +132,505 @@ public static partial class PredicateExpressionPolicyExtensions {
|
|||
/// <summary>
|
||||
/// Combine a list of expressions based on the given operator enum.
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>>? CombinePredicates<T>(IEnumerable<Expression<Func<T, bool>>> predicates, PredicateExpressionPolicyExtensions.RuleOperator op) {
|
||||
public static Expression<Func<T, bool>>? CombinePredicates<T>(IEnumerable<Expression<Func<T, bool>>> predicates,
|
||||
PredicateExpressionPolicyExtensions.RuleOperator op)
|
||||
{
|
||||
if (predicates.Count() == 0) return null;
|
||||
|
||||
if (op == RuleOperator.And) {
|
||||
if (op == RuleOperator.And)
|
||||
{
|
||||
return CombineAnd(predicates);
|
||||
}
|
||||
|
||||
return CombineOr(predicates);
|
||||
}
|
||||
|
||||
private static CoreExtenionFunctions funcs;
|
||||
/// <summary>
|
||||
/// Builds expressions using string member functions StartsWith, EndsWith or Contains as the comparator.
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>> AddStringPropertyExpression<T>(
|
||||
Expression<Func<T, string>> lambda, string filter, string filterType, bool ignoreCase = false) {
|
||||
|
||||
#if DEBUG
|
||||
if (!(filterType == "StartsWith" || filterType == "EndsWith" || filterType == "Contains" || filterType == "Equals"))
|
||||
{
|
||||
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(lambda.Body, Expression.Constant(null));
|
||||
|
||||
// Setup calls to: StartsWith, EndsWith, Contains, or Equals,
|
||||
// conditionally using character case neutral comparision.
|
||||
List<Expression> expressionArgs = new List<Expression>() { Expression.Constant(filter) };
|
||||
|
||||
if (ignoreCase) {
|
||||
expressionArgs.Add(Expression.Constant(StringComparison.CurrentCultureIgnoreCase));
|
||||
} else {
|
||||
expressionArgs.Add(Expression.Constant(StringComparison.CurrentCulture));
|
||||
}
|
||||
|
||||
MethodInfo methodInfo = typeof(string).GetMethod(filterType, new[] { typeof(string), typeof(StringComparison) });
|
||||
var strPredicate = Expression.Call(lambda.Body, methodInfo, expressionArgs);
|
||||
|
||||
Expression filterExpression = Expression.AndAlso(notNull, strPredicate);
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(
|
||||
filterExpression,
|
||||
lambda.Parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dynamically build an expression suitable for filtering in a Where clause
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>> GetPredicateExpressionForType<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 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? hasComparable = lType.GetInterface("IComparable");
|
||||
Type? hasCollection = lType.GetInterface("ICollection");
|
||||
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.
|
||||
if (lType != null)
|
||||
isNullable = true;
|
||||
hasComparable = lType.GetInterface("IComparable");
|
||||
}
|
||||
|
||||
|
||||
// For string comparisons using wildcards, trim the wildcard characters and pass to the comparison method
|
||||
if (lType == typeof(string))
|
||||
{
|
||||
Expression<Func<T, bool>> result;
|
||||
|
||||
// Grab the object property for use in the inner expression body
|
||||
var strParam = Expression.Lambda<Func<T, string>>(opLeft, parameter);
|
||||
|
||||
// If a string match begins with !, we negate the result.
|
||||
var negateResult = false;
|
||||
if (value.StartsWith("!"))
|
||||
{
|
||||
negateResult = true;
|
||||
value = value.TrimStart('!');
|
||||
}
|
||||
|
||||
// String comparisons which are prefixed with '~' will be evaluated ignoring case.
|
||||
// Note: when expression trees are used outside .net, such as with EF to SQL Server,
|
||||
// default case sensitivity for that environment may apply implicitly and counter to
|
||||
// filter policy intent.
|
||||
bool ignoreCase = false;
|
||||
if (value.StartsWith('~'))
|
||||
{
|
||||
ignoreCase = true;
|
||||
value = value.TrimStart('~');
|
||||
}
|
||||
|
||||
if (value.StartsWith("*") && value.EndsWith("*"))
|
||||
{
|
||||
result = AddStringPropertyExpression<T>(strParam, value.Trim('*'), "Contains", ignoreCase);
|
||||
}
|
||||
else if (value.StartsWith("*"))
|
||||
{
|
||||
result = AddStringPropertyExpression<T>(strParam, value.TrimStart('*'), "EndsWith", ignoreCase);
|
||||
}
|
||||
else if (value.EndsWith("*"))
|
||||
{
|
||||
result = AddStringPropertyExpression<T>(strParam, value.TrimEnd('*'), "StartsWith", ignoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = AddStringPropertyExpression<T>(strParam, value, "Equals", ignoreCase);
|
||||
}
|
||||
|
||||
if (negateResult)
|
||||
{
|
||||
result = Negate<T>(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else if (hasComparable == 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 });
|
||||
|
||||
opRight = Expression.Constant(opRightNumerical);
|
||||
Expression opLeftFinal = isNullable ? Expression.Convert(opLeft, lType) : opLeft;
|
||||
comparison = GetComparer(operatorPrefix.Value.Trim(), opLeftFinal, opRight);
|
||||
}
|
||||
}
|
||||
else if (hasCollection == typeof(ICollection))
|
||||
{
|
||||
return GetArrayContainsExpression<T>(property, value);
|
||||
}
|
||||
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;
|
||||
if (isNullable)
|
||||
{
|
||||
comparison = AddNullCheck<T>(opLeft, comparison);
|
||||
}
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(comparison ?? Expression.Equal(opLeft, opRight), parameter);
|
||||
}
|
||||
|
||||
|
||||
public static ExpressionGenerator GetCoreExpressionGenerator() => new PolicyToExpressionGenerator();
|
||||
|
||||
public static ExpressionGenerator GetEfExpressionGenerator() => new PolicyToEFExpressionGenerator();
|
||||
}
|
||||
|
||||
public class PolicyToExpressionGenerator : ExpressionGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds expressions using string member functions StartsWith, EndsWith or Contains as the comparator.
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>> AddStringPropertyExpression<T>(
|
||||
Expression<Func<T, string>> lambda, string filter, string filterType, bool ignoreCase = false) {
|
||||
|
||||
#if DEBUG
|
||||
if (!(filterType == "StartsWith" || filterType == "EndsWith" || filterType == "Contains" || filterType == "Equals"))
|
||||
{
|
||||
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(lambda.Body, Expression.Constant(null));
|
||||
|
||||
// Setup calls to: StartsWith, EndsWith, Contains, or Equals,
|
||||
// conditionally using character case neutral comparision.
|
||||
List<Expression> expressionArgs = new List<Expression>() { Expression.Constant(filter) };
|
||||
|
||||
if (ignoreCase) {
|
||||
expressionArgs.Add(Expression.Constant(StringComparison.CurrentCultureIgnoreCase));
|
||||
} else {
|
||||
expressionArgs.Add(Expression.Constant(StringComparison.CurrentCulture));
|
||||
}
|
||||
|
||||
MethodInfo methodInfo = typeof(string).GetMethod(filterType, new[] { typeof(string), typeof(StringComparison) });
|
||||
var strPredicate = Expression.Call(lambda.Body, methodInfo, expressionArgs);
|
||||
|
||||
Expression filterExpression = Expression.AndAlso(notNull, strPredicate);
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(
|
||||
filterExpression,
|
||||
lambda.Parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dynamically build an expression suitable for filtering in a Where clause
|
||||
/// </summary>
|
||||
public Expression<Func<T, bool>> GetPredicateExpressionForType<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 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? hasComparable = lType.GetInterface("IComparable");
|
||||
Type? hasCollection = lType.GetInterface("ICollection");
|
||||
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.
|
||||
if (lType != null)
|
||||
isNullable = true;
|
||||
hasComparable = lType.GetInterface("IComparable");
|
||||
}
|
||||
|
||||
|
||||
// For string comparisons using wildcards, trim the wildcard characters and pass to the comparison method
|
||||
if (lType == typeof(string))
|
||||
{
|
||||
Expression<Func<T, bool>> result;
|
||||
|
||||
// Grab the object property for use in the inner expression body
|
||||
var strParam = Expression.Lambda<Func<T, string>>(opLeft, parameter);
|
||||
|
||||
// If a string match begins with !, we negate the result.
|
||||
var negateResult = false;
|
||||
if (value.StartsWith("!"))
|
||||
{
|
||||
negateResult = true;
|
||||
value = value.TrimStart('!');
|
||||
}
|
||||
|
||||
// String comparisons which are prefixed with '~' will be evaluated ignoring case.
|
||||
// Note: when expression trees are used outside .net, such as with EF to SQL Server,
|
||||
// default case sensitivity for that environment may apply implicitly and counter to
|
||||
// filter policy intent.
|
||||
bool ignoreCase = false;
|
||||
if (value.StartsWith('~'))
|
||||
{
|
||||
ignoreCase = true;
|
||||
value = value.TrimStart('~');
|
||||
}
|
||||
|
||||
if (value.StartsWith("*") && value.EndsWith("*"))
|
||||
{
|
||||
result = AddStringPropertyExpression<T>(strParam, value.Trim('*'), "Contains", ignoreCase);
|
||||
}
|
||||
else if (value.StartsWith("*"))
|
||||
{
|
||||
result = AddStringPropertyExpression<T>(strParam, value.TrimStart('*'), "EndsWith", ignoreCase);
|
||||
}
|
||||
else if (value.EndsWith("*"))
|
||||
{
|
||||
result = AddStringPropertyExpression<T>(strParam, value.TrimEnd('*'), "StartsWith", ignoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = AddStringPropertyExpression<T>(strParam, value, "Equals", ignoreCase);
|
||||
}
|
||||
|
||||
if (negateResult)
|
||||
{
|
||||
result = Negate<T>(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else if (hasComparable == 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 });
|
||||
|
||||
opRight = Expression.Constant(opRightNumerical);
|
||||
Expression opLeftFinal = isNullable ? Expression.Convert(opLeft, lType) : opLeft;
|
||||
comparison = GetComparer(operatorPrefix.Value.Trim(), opLeftFinal, opRight);
|
||||
}
|
||||
}
|
||||
else if (hasCollection == typeof(ICollection))
|
||||
{
|
||||
return GetArrayContainsExpression<T>(property, value);
|
||||
}
|
||||
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;
|
||||
if (isNullable)
|
||||
{
|
||||
comparison = AddNullCheck<T>(opLeft, comparison);
|
||||
}
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(comparison ?? Expression.Equal(opLeft, opRight), parameter);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generate an expression tree targeting an object type based on a given policy.
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>>? GetPredicateExpression<T>(this ExpressionRuleCollection policy) {
|
||||
public Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionRuleCollection policy)
|
||||
{
|
||||
|
||||
CoreExtenionFunctions stdFuncs = new CoreExtensions();
|
||||
if (funcs == null) funcs = stdFuncs;
|
||||
var predicates = new List<Expression<Func<T, bool>>>();
|
||||
var typeName = typeof(T).Name;
|
||||
foreach (var rule in policy.Rules.Where(x => x.TargetType != null))
|
||||
{
|
||||
if (!(typeof(T).Name.Equals(rule.TargetType, StringComparison.CurrentCultureIgnoreCase)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var expression = rule.GetPredicateExpression<T>();
|
||||
if (expression != null) predicates.Add(expression);
|
||||
}
|
||||
|
||||
var expressions = CombinePredicates<T>(predicates, policy.RuleOperator);
|
||||
|
||||
if (expressions == null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"No predicates available for type: <{typeof(T).Name}> in policy: {policy.Id}");
|
||||
return PredicateBuilder.False<T>();
|
||||
}
|
||||
|
||||
return expressions;
|
||||
}
|
||||
}
|
||||
public class PolicyToEFExpressionGenerator : ExpressionGenerator
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Dynamically build an expression suitable for filtering in a Where clause
|
||||
/// </summary>
|
||||
public Expression<Func<T, bool>> GetPredicateExpressionForType<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 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? hasComparable = lType.GetInterface("IComparable");
|
||||
Type? hasCollection = lType.GetInterface("ICollection");
|
||||
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.
|
||||
if (lType != null)
|
||||
isNullable = true;
|
||||
hasComparable = lType.GetInterface("IComparable");
|
||||
}
|
||||
|
||||
|
||||
// For string comparisons using wildcards, trim the wildcard characters and pass to the comparison method
|
||||
if (lType == typeof(string))
|
||||
{
|
||||
Expression<Func<T, bool>> result;
|
||||
|
||||
// Grab the object property for use in the inner expression body
|
||||
var strParam = Expression.Lambda<Func<T, string>>(opLeft, parameter);
|
||||
|
||||
// If a string match begins with !, we negate the result.
|
||||
var negateResult = false;
|
||||
if (value.StartsWith("!"))
|
||||
{
|
||||
negateResult = true;
|
||||
value = value.TrimStart('!');
|
||||
}
|
||||
|
||||
// String comparisons which are prefixed with '~' will be evaluated ignoring case.
|
||||
// Note: when expression trees are used outside .net, such as with EF to SQL Server,
|
||||
// default case sensitivity for that environment may apply implicitly and counter to
|
||||
// filter policy intent.
|
||||
bool ignoreCase = false;
|
||||
if (value.StartsWith('~'))
|
||||
{
|
||||
ignoreCase = true;
|
||||
value = value.TrimStart('~');
|
||||
}
|
||||
|
||||
if (value.StartsWith("*") && value.EndsWith("*"))
|
||||
{
|
||||
result = this.AddStringPropertyExpression<T>(strParam, value.Trim('*'), "Contains", ignoreCase);
|
||||
}
|
||||
else if (value.StartsWith("*"))
|
||||
{
|
||||
result = this.AddStringPropertyExpression<T>(strParam, value.TrimStart('*'), "EndsWith", ignoreCase);
|
||||
}
|
||||
else if (value.EndsWith("*"))
|
||||
{
|
||||
result = this.AddStringPropertyExpression<T>(strParam, value.TrimEnd('*'), "StartsWith", ignoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = this.AddStringPropertyExpression<T>(strParam, value, "Equals", ignoreCase);
|
||||
}
|
||||
|
||||
if (negateResult)
|
||||
{
|
||||
result = Negate<T>(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else if (hasComparable == 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 });
|
||||
|
||||
opRight = Expression.Constant(opRightNumerical);
|
||||
Expression opLeftFinal = isNullable ? Expression.Convert(opLeft, lType) : opLeft;
|
||||
comparison = GetComparer(operatorPrefix.Value.Trim(), opLeftFinal, opRight);
|
||||
}
|
||||
}
|
||||
else if (hasCollection == typeof(ICollection))
|
||||
{
|
||||
return GetArrayContainsExpression<T>(property, value);
|
||||
}
|
||||
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;
|
||||
if (isNullable)
|
||||
{
|
||||
comparison = AddNullCheck<T>(opLeft, comparison);
|
||||
}
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(comparison ?? Expression.Equal(opLeft, opRight), parameter);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generate an expression tree targeting an object type based on a given policy.
|
||||
/// </summary>
|
||||
public Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionRuleCollection policy) {
|
||||
|
||||
Expression<Func<T, bool>>? expressions = PredicateBuilder.False<T>();
|
||||
|
||||
var predicates = new List<Expression<Func<T, bool>>>();
|
||||
var typeName = typeof(T).Name;
|
||||
|
@ -252,103 +638,26 @@ public static partial class PredicateExpressionPolicyExtensions {
|
|||
if (!(typeof(T).Name.Equals(rule.TargetType, StringComparison.CurrentCultureIgnoreCase))) {
|
||||
continue;
|
||||
}
|
||||
var expression = rule.GetExpression<T>();
|
||||
var expression = rule.GetPredicateExpression<T>(this);
|
||||
if (expression != null) predicates.Add(expression);
|
||||
}
|
||||
|
||||
var expressions = CombinePredicates<T>(predicates, policy.RuleOperator);
|
||||
expressions = CombinePredicates<T>(predicates, policy.RuleOperator);
|
||||
|
||||
if (expressions == null) {
|
||||
System.Diagnostics.Debug.WriteLine($"No predicates available for type: <{typeof(T).Name}> in policy: {policy.Id}");
|
||||
return PredicateBuilder.False<T>();
|
||||
}
|
||||
|
||||
|
||||
return expressions;
|
||||
}
|
||||
|
||||
private class EfExpressionOptions : ExpressionOptions {
|
||||
public bool SupportEF => true;
|
||||
public bool NoCache => true;
|
||||
}
|
||||
private static ExpressionOptions efExpressionOptions = new EfExpressionOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Generate an expression tree targeting an object type based on a given policy.
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>>? GetEFPredicateExpression<T>(this ExpressionRuleCollection policy) {
|
||||
|
||||
CoreExtenionFunctions stdFuncs = new EFExtensions();
|
||||
CoreExtenionFunctions prevFuncs = null;
|
||||
if (funcs == null) {
|
||||
System.Diagnostics.Trace.WriteLine($"Extension functions were not initialized, using standard functions and best effort EF support.");
|
||||
funcs = stdFuncs;
|
||||
} else {
|
||||
// TODO don't do this at all, just instance the damn thing. EF safe-ish stuff should just be in a different namespace.
|
||||
prevFuncs = funcs;
|
||||
}
|
||||
|
||||
Expression<Func<T, bool>>? expressions = PredicateBuilder.False<T>();
|
||||
|
||||
try {
|
||||
var predicates = new List<Expression<Func<T, bool>>>();
|
||||
var typeName = typeof(T).Name;
|
||||
foreach (var rule in policy.Rules.Where(x => x.TargetType != null)) {
|
||||
if (!(typeof(T).Name.Equals(rule.TargetType, StringComparison.CurrentCultureIgnoreCase))) {
|
||||
continue;
|
||||
}
|
||||
var expression = rule.GetExpression<T>(efExpressionOptions);
|
||||
if (expression != null) predicates.Add(expression);
|
||||
}
|
||||
|
||||
expressions = CombinePredicates<T>(predicates, policy.RuleOperator);
|
||||
|
||||
if (expressions == null) {
|
||||
System.Diagnostics.Debug.WriteLine($"No predicates available for type: <{typeof(T).Name}> in policy: {policy.Id}");
|
||||
return PredicateBuilder.False<T>();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (prevFuncs != null) { funcs = prevFuncs; }
|
||||
}
|
||||
|
||||
return expressions;
|
||||
}
|
||||
|
||||
public static void Init() {
|
||||
CoreExtenionFunctions stdFuncs = new CoreExtensions();
|
||||
if (funcs == null) funcs = stdFuncs;
|
||||
}
|
||||
|
||||
public static void SetExtensionFunctions(CoreExtenionFunctions functions) {
|
||||
funcs = functions;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* TODO This is a terrible pile of hacks and I should just refactor the whole dang thing because
|
||||
* nobody is even using this yet so the API doesn't need to be stable...I just don't know really
|
||||
* what the API should look like. Extension methods are nice to use, the shorthand they provide
|
||||
* is pretty slick. I'm just not sure how to handle EF vs non-EF expressions. Perhaps different
|
||||
* namespace? External library, there's one of those in the solution know but don't know if that
|
||||
* will last.
|
||||
*
|
||||
* The two features that conflict are case-sensitive vs insensitive matches. SQL defaults to the
|
||||
* collation because string startswith, endswith and contains are all mapped to the Like function
|
||||
* by EF then translated to LIKE SQL, at that point it's up to the DB. If done externally in a
|
||||
* libary that depends on EF Core, it could map to the Like function and it's variants which
|
||||
* will use ILIKE to force case-insensitive comparisions if desired. But, then the libary can't
|
||||
* just be .netstandard 2.1.
|
||||
*
|
||||
* For right now delegates will have to do.
|
||||
*
|
||||
* Good APIs are hard.
|
||||
* */
|
||||
internal class CoreExtensions : CoreExtenionFunctions {
|
||||
/// <summary>
|
||||
/// Builds expressions using string member functions StartsWith, EndsWith or Contains as the comparator.
|
||||
/// </summary>
|
||||
public Expression<Func<T, bool>> AddStringPropertyExpression<T>(
|
||||
Expression<Func<T, string>> lambda, string filter, string filterType, bool ignoreCase = false) {
|
||||
|
||||
/// <summary>
|
||||
/// Builds expressions using string member functions StartsWith, EndsWith or Contains as the comparator.
|
||||
/// </summary>
|
||||
public Expression<Func<T, bool>> AddStringPropertyExpression<T>(
|
||||
Expression<Func<T, string>> lambda, string filter, string filterType, bool ignoreCase = false) {
|
||||
|
||||
#if DEBUG
|
||||
if (!(filterType == "StartsWith" || filterType == "EndsWith" || filterType == "Contains" || filterType == "Equals"))
|
||||
|
@ -356,60 +665,21 @@ public static partial class PredicateExpressionPolicyExtensions {
|
|||
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(lambda.Body, Expression.Constant(null));
|
||||
|
||||
// Setup calls to: StartsWith, EndsWith, Contains, or Equals,
|
||||
// conditionally using character case neutral comparision.
|
||||
List<Expression> expressionArgs = new List<Expression>() { Expression.Constant(filter) };
|
||||
|
||||
if (ignoreCase) {
|
||||
expressionArgs.Add(Expression.Constant(StringComparison.CurrentCultureIgnoreCase));
|
||||
} else {
|
||||
expressionArgs.Add(Expression.Constant(StringComparison.CurrentCulture));
|
||||
}
|
||||
|
||||
MethodInfo methodInfo = typeof(string).GetMethod(filterType, new[] { typeof(string), typeof(StringComparison) });
|
||||
var strPredicate = Expression.Call(lambda.Body, methodInfo, expressionArgs);
|
||||
|
||||
Expression filterExpression = Expression.AndAlso(notNull, strPredicate);
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(
|
||||
filterExpression,
|
||||
lambda.Parameters);
|
||||
}
|
||||
}
|
||||
|
||||
internal class EFExtensions : CoreExtenionFunctions {
|
||||
|
||||
/// <summary>
|
||||
/// Builds expressions using string member functions StartsWith, EndsWith or Contains as the comparator.
|
||||
/// </summary>
|
||||
public Expression<Func<T, bool>> AddStringPropertyExpression<T>(
|
||||
Expression<Func<T, string>> lambda, string filter, string filterType, bool ignoreCase = false) {
|
||||
|
||||
#if DEBUG
|
||||
if (!(filterType == "StartsWith" || filterType == "EndsWith" || filterType == "Contains" || filterType == "Equals"))
|
||||
{
|
||||
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(lambda.Body, Expression.Constant(null));
|
||||
MethodInfo methodInfo = typeof(string).GetMethod(filterType, new[] { typeof(string) });
|
||||
// Check that the property isn't null, otherwise we'd hit null object exceptions at runtime
|
||||
var notNull = Expression.NotEqual(lambda.Body, Expression.Constant(null));
|
||||
MethodInfo methodInfo = typeof(string).GetMethod(filterType, new[] { typeof(string) });
|
||||
|
||||
|
||||
// Setup calls to: StartsWith, EndsWith, Contains, or Equals,
|
||||
// conditionally using character case neutral comparision.
|
||||
List<Expression> expressionArgs = new List<Expression>() { Expression.Constant(filter) };
|
||||
// Setup calls to: StartsWith, EndsWith, Contains, or Equals,
|
||||
// conditionally using character case neutral comparision.
|
||||
List<Expression> expressionArgs = new List<Expression>() { Expression.Constant(filter) };
|
||||
|
||||
var strPredicate = Expression.Call(lambda.Body, methodInfo, expressionArgs);
|
||||
var strPredicate = Expression.Call(lambda.Body, methodInfo, expressionArgs);
|
||||
|
||||
Expression filterExpression = Expression.AndAlso(notNull, strPredicate);
|
||||
Expression filterExpression = Expression.AndAlso(notNull, strPredicate);
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(
|
||||
filterExpression,
|
||||
lambda.Parameters);
|
||||
}
|
||||
return Expression.Lambda<Func<T, bool>>(
|
||||
filterExpression,
|
||||
lambda.Parameters);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue