Added expression builder that explicitly supports EF. String comparison methods that would e invalid for translating to EF expression are not used.

pull/6/head
Sean McArde 2023-09-01 12:00:47 -07:00
parent afe1ca358f
commit fb0f797326
5 changed files with 101 additions and 14 deletions

View File

@ -1,3 +1,8 @@
using Newtonsoft.Json.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Metadata;
namespace McRule.Tests {
public class Filtering {
@ -193,5 +198,29 @@ namespace McRule.Tests {
// Should be either a viking or dead, not neither.
Assert.Null(folks.FirstOrDefault(x => x.kind != "Viking" && x.stillWithUs == true));
}
[Test]
public void PolicyEFExpressionShouldNotEmitComparisonTypeStringMatches() {
var filter = eans.GetPredicateExpression<People>();
var efFilter = eans.GetEFPredicateExpression<People>();
Assert.NotNull(filter);
Assert.NotNull(efFilter.ToString(), filter.ToString());
Assert.IsTrue(filter.ToString().Contains("CurrentCulture"));
Assert.IsFalse(efFilter.ToString().Contains("CurrentCulture"));
}
[Test]
public void InvalidStringFilterTypeShouldThrow() {
var parameter = Expression.Parameter(typeof(People), "x");
var opRight = Expression.Constant("foo");
var strParam = Expression.Lambda<Func<People, string>>(opRight, parameter);
#if DEBUG
Assert.Throws(Is.TypeOf<Exception>()
.And.Message.EqualTo("filterType must equal StartsWith, EndsWith or Contains. Passed: NotAMatch"),
() => PredicateExpressionPolicyExtensions.AddStringPropertyExpression<People>(strParam, "foo", "NotAMatch"));
#endif
}
}
}

View File

@ -7,5 +7,10 @@ namespace McRule {
public interface IExpressionRule {
string TargetType { get; set; }
Expression<Func<T, bool>>? GetExpression<T>();
Expression<Func<T, bool>>? GetExpression<T>(ExpressionOptions options);
}
public interface ExpressionOptions {
public bool SupportEF { get; }
}
}

View File

@ -20,6 +20,14 @@ namespace McRule {
? 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 class ExpressionRule : IExpressionRule {
@ -58,6 +66,15 @@ namespace McRule {
return (Expression<Func<T, bool>>)cachedExpression;
}
/// <summary>
/// Returns an expression tree targeting an object type based on policy parameters.
/// </summary>
public Expression<Func<T, bool>>? GetExpression<T>(ExpressionOptions options) {
if (!(typeof(T).Name.Equals(this.TargetType, StringComparison.CurrentCultureIgnoreCase))) return null;
return PredicateExpressionPolicyExtensions.GetPredicateExpressionForType<T>(this.Property, this.Value, options.SupportEF);
}
public override string ToString() {
return $"[{TargetType}]{Property}='{Value}']";
}

View File

@ -8,7 +8,7 @@
<UserSecretsId>63a98c68-03bd-4069-b6b9-e0978081c430</UserSecretsId>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Title>McRule - Rule based expression generator</Title>
<Version>0.1.2</Version>
<Version>0.2.0</Version>
<Company>Sean McArdle</Company>
<Description>Library for generating expression trees from simple policy rules.</Description>
<Copyright>2023 Sean McArdle</Copyright>

View File

@ -20,27 +20,33 @@ public static class PredicateExpressionPolicyExtensions {
/// 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)
Expression<Func<T, string>> lambda, string filter, string filterType, bool ignoreCase = false, bool supportEF = false)
{
#if DEBUG
if (!(filterType == "StartsWith" || filterType == "EndsWith" || filterType == "Contains" || filterType == "Equals"))
{
throw new Exception($"filterType must equal StartsWith, EndsWith or Contains. Passed {filterType}");
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));
var notNull = Expression.NotEqual(lambda.Body, Expression.Constant(null));
// Setup calls to: StartsWith, EndsWith, Contains, or Equals,
// conditionally using character case neutral comparision.
Expression[] expressionArgs = new[] { Expression.Constant(filter), Expression.Constant(StringComparison.CurrentCulture) };
if (ignoreCase)
{
expressionArgs[1] = Expression.Constant(StringComparison.CurrentCultureIgnoreCase);
List<Expression> expressionArgs = new List<Expression>() { Expression.Constant(filter) };
if (supportEF) {
ignoreCase = false;
} else {
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) });
MethodInfo methodInfo = supportEF ? typeof(string).GetMethod(filterType, new[] { typeof(string) })
: typeof(string).GetMethod(filterType, new[] { typeof(string), typeof(StringComparison) });
var strPredicate = Expression.Call(lambda.Body, methodInfo, expressionArgs);
Expression filterExpression = Expression.AndAlso(notNull, strPredicate);
@ -103,7 +109,7 @@ public static class PredicateExpressionPolicyExtensions {
/// <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) {
public static Expression<Func<T, bool>> GetPredicateExpressionForType<T>(string property, string value, bool supportEF=false) {
var parameter = Expression.Parameter(typeof(T), "x");
var opLeft = Expression.Property(parameter, property);
var opRight = Expression.Constant(value);
@ -154,13 +160,13 @@ public static class PredicateExpressionPolicyExtensions {
}
if (value.StartsWith("*") && value.EndsWith("*")) {
result = AddStringPropertyExpression<T>(strParam, value.Trim('*'), "Contains", ignoreCase);
result = AddStringPropertyExpression<T>(strParam, value.Trim('*'), "Contains", ignoreCase, supportEF);
} else if (value.StartsWith("*")) {
result = AddStringPropertyExpression<T>(strParam, value.TrimStart('*'), "EndsWith", ignoreCase);
result = AddStringPropertyExpression<T>(strParam, value.TrimStart('*'), "EndsWith", ignoreCase, supportEF);
} else if (value.EndsWith("*")) {
result = AddStringPropertyExpression<T>(strParam, value.TrimEnd('*'), "StartsWith", ignoreCase);
result = AddStringPropertyExpression<T>(strParam, value.TrimEnd('*'), "StartsWith", ignoreCase, supportEF);
} else {
result = AddStringPropertyExpression<T>(strParam, value, "Equals", ignoreCase);
result = AddStringPropertyExpression<T>(strParam, value, "Equals", ignoreCase, supportEF);
}
if (negateResult) {
@ -289,4 +295,34 @@ public static class PredicateExpressionPolicyExtensions {
return expressions;
}
private class EfExpressionOptions : ExpressionOptions {
public bool SupportEF => 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) {
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);
}
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;
}
}