Removed the static GenerateExpressionPolicyForType and fleshed out more interfaces. Need testing with nested policies which rely on a specific expression generator for correctness.

sean-m-patch-1
Sean McArde 2023-09-07 17:39:25 -07:00
parent 484f517979
commit aef5190f3e
8 changed files with 95 additions and 183 deletions

View File

@ -93,7 +93,7 @@ namespace McRule.Tests {
("People", "name", "*ean").ToFilterRule(),
("People", "name", "~*EAN").ToFilterRule(),
},
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.And
RuleOperator = RuleOperator.And
};
ExpressionPolicy youngens = new ExpressionPolicy {
@ -104,7 +104,7 @@ namespace McRule.Tests {
("People", "number", ">=17").ToFilterRule(),
("People", "number", "<30").ToFilterRule(),
},
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.And
RuleOperator = RuleOperator.And
};
ExpressionPolicy vikings = new ExpressionPolicy {
@ -113,7 +113,7 @@ namespace McRule.Tests {
{
("People", "kind", "~viking").ToFilterRule(),
},
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.And
RuleOperator = RuleOperator.And
};
ExpressionPolicy muggles = new ExpressionPolicy {
@ -122,7 +122,7 @@ namespace McRule.Tests {
{
("People", "tags", "muggle").ToFilterRule(),
},
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.And
RuleOperator = RuleOperator.And
};
ExpressionPolicy notQuiteDead = new ExpressionPolicy {
@ -130,7 +130,7 @@ namespace McRule.Tests {
{
("People", "stillWithUs", "true").ToFilterRule(),
},
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.And
RuleOperator = RuleOperator.And
};
ExpressionPolicy deadOrViking = new ExpressionPolicy {
@ -139,7 +139,7 @@ namespace McRule.Tests {
("People", "stillWithUs", "false").ToFilterRule(),
("People", "kind", "Viking").ToFilterRule(),
},
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.Or
RuleOperator = RuleOperator.Or
};
#endregion testPolicies
@ -156,7 +156,7 @@ namespace McRule.Tests {
// Test using EF generator
var efGenerator = PredicateExpressionPolicyExtensions.GetEfExpressionGenerator();
var efFilter = matchNullLiteral.GetPredicateExpression<People>(efGenerator)?.Compile();
var efFilter = matchNullLiteral.GeneratePredicateExpression<People>(efGenerator)?.Compile();
folks = peoples.Where(efFilter);
@ -173,7 +173,7 @@ namespace McRule.Tests {
// Test using EF generator
var efGenerator = PredicateExpressionPolicyExtensions.GetEfExpressionGenerator();
var efFilter = matchNullByString.GetPredicateExpression<People>(efGenerator)?.Compile();
var efFilter = matchNullByString.GeneratePredicateExpression<People>(efGenerator)?.Compile();
folks = peoples.Where(efFilter);
@ -206,7 +206,7 @@ namespace McRule.Tests {
Rules = new[] {
youngens, vikings
},
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.And
RuleOperator = RuleOperator.And
}?.GetPredicateExpression<People>()?.Compile();
var folks = peoples.Where(filter);
@ -231,7 +231,7 @@ namespace McRule.Tests {
Rules = new[] {
youngens, vikings
},
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.Or
RuleOperator = RuleOperator.Or
}?.GetPredicateExpression<People>()?.Compile();
folks = peoples.Where(filter);
@ -254,11 +254,11 @@ namespace McRule.Tests {
public void FilterListOfObjectsByMemberCollectionContents()
{
var generator = new PolicyToExpressionGenerator();
var generatedFilter = muggles.GetPredicateExpression<People>(generator);
var generatedFilter = muggles.GeneratePredicateExpression<People>(generator);
var filteredFolks = peoples.Where(generatedFilter.Compile());
var efGenerator = new PolicyToEFExpressionGenerator();
var efGeneratedFilter = muggles.GetPredicateExpression<People>(efGenerator);
var efGeneratedFilter = muggles.GeneratePredicateExpression<People>(efGenerator);
var efFilteredFolks = peoples.Where(efGeneratedFilter.Compile());
@ -281,11 +281,9 @@ namespace McRule.Tests {
}
[Test]
public void NullFilterWhenNoMatchingTypes() {
public void FilterWhenNoMatchingTypesThrows() {
// Shouldn't have any filters in the policy for string objects.
var filter = notQuiteDead.GetPredicateExpression<string>()?.Compile();
Assert.Null(filter);
Assert.Throws<ExpressionGeneratorException>(() => { notQuiteDead.GetPredicateExpression<string>(); });
}
[Test]
@ -314,7 +312,7 @@ namespace McRule.Tests {
var filter = eans.GetPredicateExpression<People>();
var efGenerator = PredicateExpressionPolicyExtensions.GetEfExpressionGenerator();
var efFilter = eans.GetPredicateExpression<People>(efGenerator);
var efFilter = eans.GeneratePredicateExpression<People>(efGenerator);
Assert.NotNull(filter);
Assert.AreNotEqual(efFilter.ToString(), filter.ToString());
@ -326,7 +324,7 @@ namespace McRule.Tests {
public void BaseThrowsOnPoorlyImplementedGenerator()
{
var failedGenerator = new FailedExpressionGeneratorBase();
Assert.Throws<NotImplementedException>(() => { _ = eans.GetPredicateExpression<People>(failedGenerator); });
Assert.Throws<NotImplementedException>(() => { _ = eans.GeneratePredicateExpression<People>(failedGenerator); });
}
}

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace McRule {
public class ExpressionGeneratorException : Exception {
public ExpressionGeneratorException() { }
public ExpressionGeneratorException(string message) : base(message) { }
public ExpressionGeneratorException(string message, Exception innerException) : base(message, innerException) { }
}
}

View File

@ -4,16 +4,32 @@ using System.Linq.Expressions;
using System.Text;
namespace McRule {
public interface IExpressionRuleCollection {
public Guid Id { get; }
public IEnumerable<IExpressionPolicy> Rules { get; }
public RuleOperator RuleOperator { get; }
}
public interface IExpressionRule {
string TargetType { get; set; }
string Property { get; set; }
string Value { get; set; }
}
public interface IExpressionPolicy {
Expression<Func<T, bool>>? GetPredicateExpression<T>();
Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionGenerator generator);
Expression<Func<T, bool>>? GeneratePredicateExpression<T>(ExpressionGenerator generator);
}
public interface ExpressionGenerator
{
Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionRuleCollection policy);
Expression<Func<T, bool>>? GetPredicateExpressionOrFalse<T>(IExpressionPolicy policy);
Expression<Func<T, bool>>? GetPredicateExpression<T>(IExpressionRule policy);
Expression<Func<T, bool>>? GetPredicateExpression<T>(IExpressionRuleCollection policy);
Expression<Func<T, bool>> GetPredicateExpressionForType<T>(string property, string value);
}
}

View File

@ -3,32 +3,33 @@ using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using static McRule.PredicateExpressionPolicyExtensions;
namespace McRule {
public class ExpressionRuleCollection : IExpressionRule {
public class ExpressionRuleCollection : IExpressionRuleCollection, IExpressionPolicy {
public Guid Id { get; set; } = Guid.NewGuid();
public PredicateExpressionPolicyExtensions.RuleOperator RuleOperator { get; set; } = PredicateExpressionPolicyExtensions.RuleOperator.And;
public IEnumerable<IExpressionRule> Rules { get; set; }
public string Name { get; set; }
public RuleOperator RuleOperator { get; set; } = RuleOperator.And;
public IEnumerable<IExpressionPolicy> Rules { get; set; }
public string TargetType { get; set; }
public ExpressionRuleCollection() { }
public Expression<Func<T, bool>>? GetPredicateExpression<T>() {
var expressions = Rules.Select(x => x.GetPredicateExpression<T>()).Where(x => x != null);
var gen = PredicateExpressionPolicyExtensions.GetCoreExpressionGenerator();
return (RuleOperator == PredicateExpressionPolicyExtensions.RuleOperator.Or)
? PredicateExpressionPolicyExtensions.CombineOr<T>(expressions)
: PredicateExpressionPolicyExtensions.CombineAnd<T>(expressions);
return GeneratePredicateExpression<T>(gen);
}
public Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionGenerator generator)
public Expression<Func<T, bool>>? GeneratePredicateExpression<T>(ExpressionGenerator generator)
{
var expressions = generator.GetPredicateExpression<T>(this);
return expressions;
}
}
public class ExpressionRule : IExpressionRule {
public class ExpressionRule : IExpressionRule, IExpressionPolicy {
public string TargetType { get; set; }
public string Property { get; set; }
public string Value { get; set; }
@ -56,7 +57,7 @@ namespace McRule {
if (!(typeof(T).Name.Equals(this.TargetType, StringComparison.CurrentCultureIgnoreCase))) return null;
if (cachedExpression == null) {
cachedExpression = PredicateExpressionPolicyExtensions.GetPredicateExpressionForType<T>(this.Property, this.Value);
cachedExpression = GetCoreExpressionGenerator().GetPredicateExpressionForType<T>(this.Property, this.Value);
}
return (Expression<Func<T, bool>>)cachedExpression;
@ -65,7 +66,7 @@ namespace McRule {
/// <summary>
/// Returns an expression tree targeting an object type based on policy parameters.
/// </summary>
public Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionGenerator generator) {
public Expression<Func<T, bool>>? GeneratePredicateExpression<T>(ExpressionGenerator generator) {
if (!(typeof(T).Name.Equals(this.TargetType, StringComparison.CurrentCultureIgnoreCase))) return null;
return generator.GetPredicateExpressionForType<T>(this.Property, this.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.2.2</Version>
<Version>0.3.0</Version>
<Company>Sean McArdle</Company>
<Description>Library for generating expression trees from simple policy rules.</Description>
<Copyright>2023 Sean McArdle</Copyright>

View File

@ -8,14 +8,13 @@ using static McRule.PredicateExpressionPolicyExtensions;
namespace McRule;
public enum RuleOperator {
And,
Or
}
public static partial class PredicateExpressionPolicyExtensions
{
public enum RuleOperator
{
And,
Or
}
public static ExpressionRule ToFilterRule(this (string, string, string) tuple)
{
return new ExpressionRule(tuple);
@ -143,7 +142,7 @@ public static partial class PredicateExpressionPolicyExtensions
/// 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)
RuleOperator op)
{
if (predicates.Count() == 0) return null;
@ -216,133 +215,6 @@ public static partial class PredicateExpressionPolicyExtensions
return (false, null);
}
/// <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);
(bool literalFound, LiteralValue? processedValue) = GetStringValueLiteral(value);
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");
}
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))
{
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 (hasCollection == typeof(ICollection)) {
return GetArrayContainsExpression<T>(property, value);
}
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
{
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 = AddNotNullCheck<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();
@ -534,21 +406,16 @@ public abstract class ExpressionGeneratorBase : ExpressionGenerator
/// <summary>
/// Generate an expression tree targeting an object type based on a given policy.
/// </summary>
public Expression<Func<T, bool>>? GetPredicateExpression<T>(ExpressionRuleCollection policy)
public Expression<Func<T, bool>>? GetPredicateExpression<T>(IExpressionRuleCollection policy)
{
Expression<Func<T, bool>>? expressions = PredicateBuilder.False<T>();
var predicates = new List<Expression<Func<T, bool>>>();
var typeName = typeof(T).Name;
foreach (var rule in policy.Rules.Where(x => x.TargetType != null))
var selectedType = typeof(T);
foreach (var rule in policy.Rules)
{
if (!(typeof(T).Name.Equals(rule.TargetType, StringComparison.CurrentCultureIgnoreCase)))
{
continue;
}
var expression = rule.GetPredicateExpression<T>(this);
if (expression != null) predicates.Add(expression);
var expression = GetPredicateExpression<T>(rule);
if (expression != null) { predicates.Add(expression); }
}
expressions = CombinePredicates<T>(predicates, policy.RuleOperator);
@ -556,10 +423,28 @@ public abstract class ExpressionGeneratorBase : ExpressionGenerator
if (expressions == null)
{
System.Diagnostics.Debug.WriteLine(
$"No predicates available for type: <{typeof(T).Name}> in policy: {policy.Id}");
return PredicateBuilder.False<T>();
$"No predicates available for type: <{selectedType.Name}> in policy: {policy.Id}");
throw new ExpressionGeneratorException($"No filter expressions found for type: {selectedType.Name} FullName: {selectedType.FullName}");
}
return expressions;
}
public Expression<Func<T, bool>>? GetPredicateExpression<T>(IExpressionPolicy policy) {
return policy.GeneratePredicateExpression<T>(this);
}
public Expression<Func<T, bool>>? GetPredicateExpression<T>(IExpressionRule rule) {
if (!string.Equals(typeof(T).Name, rule.TargetType, StringComparison.CurrentCultureIgnoreCase)) return null;
return GetPredicateExpressionForType<T>(rule.Property, rule.Value);
}
public Expression<Func<T, bool>>? GetPredicateExpressionOrFalse<T>(IExpressionPolicy policy) {
var expression = PredicateBuilder.False<T>();
try { expression = GetPredicateExpression<T>(policy); }
catch { /* Yeah, that didn't work. */ }
return expression;
}
}

View File

@ -105,7 +105,7 @@ public static class FilterRuleManager
return sequence.Where(x => false);
}
var predicates = PredicateExpressionPolicyExtensions.CombinePredicates(rules, PredicateExpressionPolicyExtensions.RuleOperator.And).Compile();
var predicates = PredicateExpressionPolicyExtensions.CombinePredicates(rules, RuleOperator.And).Compile();
return sequence.Where(predicates);
}

View File

@ -4,9 +4,10 @@ using System;
using System.Linq;
using System.Linq.Expressions;
using System.Collections.Generic;
using static McRule.PredicateExpressionPolicyExtensions;
using static RulerDev.MyExtensions;
var users = new List<User> {
new User("Sean","McArdle","971-900-7335","503-555-1245", "SDC", "DAS", new string[] {"IT", "Admin"}),
new User("Brian","Chamberland","971-900-7335","503-555-1245", "SDC", "DAS"),
@ -32,7 +33,7 @@ var filterPolicy = new ExpressionPolicy
("User", "agency", "OHA").ToFilterRule(),
("Group", "agency", "OHA").ToFilterRule(),
},
RuleOperator = PredicateExpressionPolicyExtensions.RuleOperator.Or
RuleOperator = RuleOperator.Or
};
Expression<Func<User,bool>>? filterExpression = filterPolicy.GetPredicateExpression<User>();
filterPolicy.Dump(filterPolicy.Name);
@ -68,10 +69,10 @@ filterExpression = filterPolicy.GetPredicateExpression<User>();
filterPolicy.Dump(filterPolicy.Name);
users.Where(filterExpression).Dump($"operator {filterPolicy.RuleOperator}");
var dynamicFilterDAS = PredicateExpressionPolicyExtensions.GetPredicateExpressionForType<User>("agency", "DAS");
var dynamicFilterDAS = GetCoreExpressionGenerator().GetPredicateExpressionForType<User>("agency", "DAS");
users.Where(dynamicFilterDAS).Dump("DAS users");
var dynamicFilterBrian = PredicateExpressionPolicyExtensions.GetPredicateExpressionForType<User>("first","Brian");
var dynamicFilterBrian = GetCoreExpressionGenerator().GetPredicateExpressionForType<User>("first","Brian");
var policies = new List<Expression<Func<User,bool>>>();
policies.Add(dynamicFilterDAS);