mirror of https://github.com/sean-m/McRule.git
Compare commits
7 Commits
e5af19f7fb
...
cdc819d2f5
Author | SHA1 | Date |
---|---|---|
Sean McArde | cdc819d2f5 | |
Sean McArde | 6e523ce7a8 | |
Sean McArde | 0481f352d5 | |
Sean McArde | cd24d36aa7 | |
Sean McArde | e6389cab1d | |
Sean McArde | a4a2b12db4 | |
Sean McArde | a3f5324776 |
|
@ -4,6 +4,8 @@ using System.Linq.Expressions;
|
|||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
|
||||
using static McRule.Tests.TestPolicies;
|
||||
|
||||
namespace McRule.Tests {
|
||||
public class Filtering {
|
||||
|
||||
|
@ -48,102 +50,6 @@ namespace McRule.Tests {
|
|||
}
|
||||
}
|
||||
|
||||
#region testPolicies
|
||||
|
||||
ExpressionPolicy everyKindInclusive = new ExpressionPolicy {
|
||||
Name = "Any kind including null",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "*").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
ExpressionPolicy matchNullLiteral = new ExpressionPolicy {
|
||||
Name = "Any kind null",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "{{NULL}}").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
ExpressionPolicy matchNullByString = new ExpressionPolicy {
|
||||
Name = "Don't think this should work",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "null").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
ExpressionPolicy notSean = new ExpressionPolicy {
|
||||
Name = "Not named Sean",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "name", "!Sean").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
ExpressionPolicy eans = new ExpressionPolicy {
|
||||
Name = "eans",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "name", "*ean").ToFilterRule(),
|
||||
("People", "name", "~*EAN").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
ExpressionPolicy youngens = new ExpressionPolicy {
|
||||
Name = "Young folk",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "number", ">=17").ToFilterRule(),
|
||||
("People", "number", "<30").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
ExpressionPolicy vikings = new ExpressionPolicy {
|
||||
Name = "Vikings",
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "~viking").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
ExpressionPolicy muggles = new ExpressionPolicy {
|
||||
Name = "Non-magic folk",
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "tags", "muggle").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
ExpressionPolicy notQuiteDead = new ExpressionPolicy {
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "stillWithUs", "true").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
ExpressionPolicy deadOrViking = new ExpressionPolicy {
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "stillWithUs", "false").ToFilterRule(),
|
||||
("People", "kind", "Viking").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.Or
|
||||
};
|
||||
|
||||
#endregion testPolicies
|
||||
|
||||
[SetUp]
|
||||
public void Setup() { }
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using static McRule.Tests.TestPolicies;
|
||||
|
||||
namespace McRule.Tests {
|
||||
public class IDictionarySelector {
|
||||
|
||||
public class SomeContext {
|
||||
public string Name { get; set; }
|
||||
public bool Authorized { get; set; } = false;
|
||||
public ContextStringDictionary Context { get; set; }
|
||||
}
|
||||
|
||||
public class ContextStringDictionary : Dictionary<string, string> { }
|
||||
|
||||
public List<SomeContext> SomeContexts = new List<SomeContext>() {
|
||||
new SomeContext {
|
||||
Name = "Me",
|
||||
Context = new ContextStringDictionary() {
|
||||
{ "GivenName", "Sean"},
|
||||
{ "Surname", "McArdle" },
|
||||
{ "Department", "IT" },
|
||||
{ "Team", "Cloud" },
|
||||
}
|
||||
},
|
||||
new SomeContext {
|
||||
Name = "Dog",
|
||||
Context = new ContextStringDictionary() {
|
||||
{ "GivenName", "Navi"},
|
||||
{ "Surname", "McArdle" },
|
||||
{ "Department", "Security" },
|
||||
{ "Team", "Pets" },
|
||||
}
|
||||
},
|
||||
new SomeContext {
|
||||
Name = "Son",
|
||||
Context = new ContextStringDictionary() {
|
||||
{ "GivenName", "Thing-1"},
|
||||
{ "Surname", "McArdle" },
|
||||
{ "Department", "IT" },
|
||||
{ "Team", "Children" },
|
||||
}
|
||||
},
|
||||
new SomeContext() {
|
||||
Name = "Son",
|
||||
Context = new ContextStringDictionary() {
|
||||
{ "GivenName", "Thing-2"},
|
||||
{ "Surname", "McArdle" },
|
||||
{ "Team", "Children" },
|
||||
}
|
||||
},
|
||||
new SomeContext() {
|
||||
Name = "Nerd Son",
|
||||
Context = new ContextStringDictionary() {
|
||||
{ "GivenName", "Thing-2"},
|
||||
{ "Surname", "McArdle" },
|
||||
{ "Team", "Children" },
|
||||
{ "Department", "it" },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[SetUp] public void SetUp() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void CanSelectDictionaryValuesByKey() {
|
||||
var lambda = itPeople.GetPredicateExpression<ContextStringDictionary>();
|
||||
|
||||
var filter = lambda.Compile();
|
||||
var localContext = SomeContexts;
|
||||
var filteredContexts = localContext.Select(x => x.Context)
|
||||
.Where(x => x.ContainsKey("Department")) // Skip entry with a missing key
|
||||
.Where(filter);
|
||||
|
||||
Assert.NotNull(filteredContexts);
|
||||
Assert.AreEqual(filteredContexts.Count(), 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanSelectDictionaryValuesByKeyCaseInsensitive() {
|
||||
var lambda = itPeopleCaseless.GetPredicateExpression<ContextStringDictionary>();
|
||||
|
||||
var filter = lambda.Compile();
|
||||
var localContext = SomeContexts;
|
||||
var filteredContexts = localContext.Select(x => x.Context)
|
||||
.Where(x => x.ContainsKey("Department")) // Skip entry with a missing key
|
||||
.Where(filter);
|
||||
|
||||
Assert.NotNull(filteredContexts);
|
||||
Assert.AreEqual(filteredContexts.Count(), 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanSelectDictionaryValueWithContainsKeyCheck() {
|
||||
var lambda = itPeople.GetPredicateExpression<ContextStringDictionary>();
|
||||
|
||||
var filter = lambda.Compile();
|
||||
var localContext = SomeContexts;
|
||||
var filteredContexts = localContext.Select(x => x.Context)
|
||||
.Where(filter);
|
||||
|
||||
Assert.NotNull(filteredContexts);
|
||||
|
||||
int filteredCount = filteredContexts.Count();
|
||||
Assert.AreEqual(filteredCount, 2);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
using McRule;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace McRule.Tests {
|
||||
public static class TestPolicies {
|
||||
|
||||
#region peopleTests
|
||||
|
||||
public static ExpressionPolicy everyKindInclusive = new ExpressionPolicy {
|
||||
Name = "Any kind including null",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "*").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
public static ExpressionPolicy matchNullLiteral = new ExpressionPolicy {
|
||||
Name = "Any kind null",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "{{NULL}}").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
public static ExpressionPolicy matchNullByString = new ExpressionPolicy {
|
||||
Name = "Don't think this should work",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "null").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
public static ExpressionPolicy notSean = new ExpressionPolicy {
|
||||
Name = "Not named Sean",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "name", "!Sean").ToFilterRule(),
|
||||
}
|
||||
};
|
||||
|
||||
public static ExpressionPolicy eans = new ExpressionPolicy {
|
||||
Name = "eans",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "name", "*ean").ToFilterRule(),
|
||||
("People", "name", "~*EAN").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
public static ExpressionPolicy youngens = new ExpressionPolicy {
|
||||
Name = "Young folk",
|
||||
Properties = new string[] { }, // Can't do anything with this yet
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "number", ">=17").ToFilterRule(),
|
||||
("People", "number", "<30").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
public static ExpressionPolicy vikings = new ExpressionPolicy {
|
||||
Name = "Vikings",
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "kind", "~viking").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
public static ExpressionPolicy muggles = new ExpressionPolicy {
|
||||
Name = "Non-magic folk",
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "tags", "muggle").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
public static ExpressionPolicy notQuiteDead = new ExpressionPolicy {
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "stillWithUs", "true").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
public static ExpressionPolicy deadOrViking = new ExpressionPolicy {
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("People", "stillWithUs", "false").ToFilterRule(),
|
||||
("People", "kind", "Viking").ToFilterRule(),
|
||||
},
|
||||
RuleOperator = RuleOperator.Or
|
||||
};
|
||||
|
||||
#endregion testPolicies
|
||||
|
||||
#region someContext policies
|
||||
|
||||
public static ExpressionPolicy itPeople = new ExpressionPolicy {
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("ContextDictionary", "Department", "IT").ToFilterRule(),
|
||||
("ContextStringDictionary", "Department", "IT").ToFilterRule(),
|
||||
("SomeContext", "Context.Department", "IT").ToFilterRule(), // Same rule but with nested selector
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
public static ExpressionPolicy itPeopleCaseless = new ExpressionPolicy {
|
||||
Rules = new List<ExpressionRule>
|
||||
{
|
||||
("ContextDictionary", "Department", "~IT").ToFilterRule(),
|
||||
("ContextStringDictionary", "Department", "~IT").ToFilterRule(),
|
||||
("SomeContext", "Context.Department", "~IT").ToFilterRule(), // Same rule but with nested selector
|
||||
},
|
||||
RuleOperator = RuleOperator.And
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
<UserSecretsId>63a98c68-03bd-4069-b6b9-e0978081c430</UserSecretsId>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<Title>McRule - Rule based expression generator</Title>
|
||||
<Version>0.3.1</Version>
|
||||
<Version>4.0.0</Version>
|
||||
<Company>Sean McArdle</Company>
|
||||
<Description>Library for generating expression trees from simple policy rules.</Description>
|
||||
<Copyright>2023 Sean McArdle</Copyright>
|
||||
|
|
|
@ -34,6 +34,29 @@ public static partial class PredicateExpressionPolicyExtensions
|
|||
return Expression.AndAlso(notNull, expression);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an expression which executes a ContainsKey method call on IDictionary types
|
||||
/// and prepends it to a given expression with an AndAlso operator. Note, this comparison
|
||||
/// short circuits so the right hand side will not execute when a key is not found.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="left"></param>
|
||||
/// <param name="right"></param>
|
||||
/// <param name="dictKey"></param>
|
||||
/// <returns></returns>
|
||||
internal static Expression AddContainsKeyCheck<T>(
|
||||
Expression left,
|
||||
string dictKey,
|
||||
Expression<Func<T, bool>> right) {
|
||||
|
||||
// Create generic method which is bound with the Call Expression below
|
||||
var containsKeyRuntimeMethod = left.Type.GetMethod("ContainsKey");
|
||||
|
||||
var containsKeyCall = Expression.Call(left, containsKeyRuntimeMethod, Expression.Constant(dictKey));
|
||||
var methodExpression = Expression.Lambda<Func<T, bool>>(containsKeyCall, false, right.Parameters);
|
||||
return PredicateBuilder.And<T>(methodExpression, right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test for null value. This is used to test for null literals.
|
||||
/// </summary>
|
||||
|
@ -277,26 +300,80 @@ public class PolicyToEFExpressionGenerator : ExpressionGeneratorBase
|
|||
}
|
||||
}
|
||||
|
||||
public abstract class ExpressionGeneratorBase : ExpressionGenerator
|
||||
{
|
||||
|
||||
|
||||
public abstract class ExpressionGeneratorBase : ExpressionGenerator {
|
||||
|
||||
|
||||
public virtual 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) {
|
||||
throw new NotImplementedException("Must override the AddStringPropertyExpression<T> method in a child class. This one is virtual and shouldn't ever be called.");
|
||||
}
|
||||
|
||||
|
||||
private class MemberResolveResult<T> {
|
||||
internal List<Expression<Func<T, bool>>> PreChecks { get; set; } = new List<Expression<Func<T, bool>>>();
|
||||
|
||||
internal Expression Member { get; set; }
|
||||
|
||||
internal bool LOpIsDict { get; set; } = false;
|
||||
internal Expression LOp { get; set; }
|
||||
internal string DictKey { get; set; }
|
||||
|
||||
internal void AddNewPreCheck(Expression<Func<T, bool>> lambda) {
|
||||
PreChecks.Add(lambda);
|
||||
}
|
||||
|
||||
internal Expression<Func<T, bool>> GetPrecheckFunc() {
|
||||
if (PreChecks.Count == 0) {
|
||||
return default(Expression<Func<T, bool>>);
|
||||
}
|
||||
|
||||
return CombineAnd<T>(PreChecks);
|
||||
}
|
||||
}
|
||||
|
||||
private MemberResolveResult<T> GetMemberByNameForType<T>(string propertyName, ParameterExpression parameter) {
|
||||
|
||||
var result = new MemberResolveResult<T>();
|
||||
|
||||
Expression opLeft = parameter;
|
||||
|
||||
foreach (string p in propertyName.Split(".")) {
|
||||
result.LOpIsDict = false;
|
||||
|
||||
if (opLeft.Type.GetInterfaces().Contains(typeof(IDictionary))) {
|
||||
result.LOpIsDict = true;
|
||||
result.DictKey = p;
|
||||
result.LOp = opLeft;
|
||||
|
||||
var dictKey = Expression.Constant(p);
|
||||
|
||||
opLeft = Expression.Property(opLeft, "Item", dictKey);
|
||||
|
||||
} else {
|
||||
opLeft = Expression.PropertyOrField(opLeft, p);
|
||||
}
|
||||
}
|
||||
result.Member = opLeft;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/// <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");
|
||||
Expression opLeft = parameter;
|
||||
foreach (string p in property.Split(".")) opLeft = Expression.PropertyOrField(opLeft, p);
|
||||
var resolvedMember = GetMemberByNameForType<T>(property, parameter);
|
||||
Expression opLeft = resolvedMember.Member;
|
||||
|
||||
|
||||
(bool literalFound, LiteralValue? processedValue) = GetStringValueLiteral(value);
|
||||
var opRight = Expression.Constant(value);
|
||||
Expression? comparison = null;
|
||||
Expression? comparison = resolvedMember.GetPrecheckFunc();
|
||||
|
||||
// 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.
|
||||
|
@ -355,6 +432,7 @@ public abstract class ExpressionGeneratorBase : ExpressionGenerator
|
|||
else if (value.StartsWith("*"))
|
||||
{
|
||||
comparison = AddStringPropertyExpression<T>(strParam, value.TrimStart('*'), "EndsWith", ignoreCase);
|
||||
|
||||
}
|
||||
else if (value.EndsWith("*"))
|
||||
{
|
||||
|
@ -409,12 +487,24 @@ public abstract class ExpressionGeneratorBase : ExpressionGenerator
|
|||
comparison = AddNotNullCheck<T>(opLeft, comparison);
|
||||
}
|
||||
|
||||
// The value may have the right type and should just be returned.
|
||||
if (comparison is Expression<Func<T, bool>> result && result != default(Expression<Func<T, bool>>)) {
|
||||
return result;
|
||||
// When the left hand side of the comparision implements IDictionary we need to add a ContainsKey
|
||||
// method call to assert there's a value to compare against before actually retrieving it by name.
|
||||
// A missing key evaluates to false.
|
||||
// TODO: use ~ operator to return true for a comparison predicate where a key is missing.
|
||||
if (resolvedMember.LOpIsDict) {
|
||||
comparison = AddContainsKeyCheck<T>(resolvedMember.LOp, resolvedMember.DictKey, (Expression<Func<T, bool>> )comparison);
|
||||
}
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(comparison ?? Expression.Equal(opLeft, opRight), parameter);
|
||||
// The value may have the right type and should just be returned.
|
||||
Expression<Func<T, bool>> result = default(Expression<Func<T, bool>>);
|
||||
if (comparison is Expression<Func<T, bool>> checkedResult && checkedResult != default(Expression<Func<T, bool>>)) {
|
||||
result = checkedResult;
|
||||
}
|
||||
else {
|
||||
result = Expression.Lambda<Func<T, bool>>(comparison ?? Expression.Equal(opLeft, opRight), parameter);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ A simple equality comparison is used by default but operators can be prefixed to
|
|||
| IComparable | <>, !=, ! | Not-equal to comparison. |
|
||||
|
||||
> Note: the IComparable interface is mostly used for numerical types but custom types with comparison providers may work at runtime.
|
||||
> Note: initial IDictionary support has been added but only for collections where the value types are strings. When missing keys are encountered, evaluation defaults to false.
|
||||
|
||||
### Literal Values
|
||||
Literal values, as needed, use handlbar syntax: {{ value }}. Null checks are implicitly added to most expressions but sometimes you need an expression that evaluates true for null values. In that case, a null literal is represented as {{null}}.
|
||||
|
|
Loading…
Reference in New Issue