mirror of https://github.com/sean-m/McRule.git
Added expression builder that explicitly supports EF. String comparison methods that would e invalid for translating to EF expression are not used.
parent
afe1ca358f
commit
fb0f797326
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}']";
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue