使用 LinqExpressions 构建 MicroRuleEngine

发布于 2024-12-03 10:16:06 字数 3969 浏览 0 评论 0原文

因此,我正在构建一个 MicroRuleEngine(希望看到它作为一个开源项目起飞),并且在执行编译的 ExpressionTree 时遇到空引用错误,我不确定为什么。针对简单属性的规则有效,但针对子属性(即 Customer.Client.Address.StreetName 等)的规则则不起作用。

下面是 MicroRuleEngine

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace Trial
{
    public class MicroRuleEngine
    {
        public bool PassesRules<T>(List<Rule> rules, T toInspect)
        {
            bool pass = true;
            foreach (var rule in rules)
            {
                var cr = this.CompileRule<T>(rule);
                pass = pass && cr.Invoke(toInspect);
                if (!pass)
                    return pass;
            }
            return pass;
        }
        public Func<T, bool> CompileRule<T>(Rule r)
        {
            var paramUser = Expression.Parameter(typeof(T));
            Expression expr = BuildExpr<T>(r, paramUser);
            // build a lambda function User->bool and compile it

            return Expression.Lambda<Func<T, bool>>(expr, paramUser).Compile();
        }

        Expression BuildExpr<T>(Rule r, ParameterExpression param)
        {
            Expression propExpression;
            Type propType;// typeof(T).GetProperty(r.MemberName).PropertyType;
            ExpressionType tBinary;
            if (r.MemberName.Contains('.'))
            {
                // support to be sorted on child fields.
                String[] childProperties = r.MemberName.Split('.');
                var property = typeof(T).GetProperty(childProperties[0]);
                var paramExp = Expression.Parameter(typeof(T), "SomeObject");
                propExpression = Expression.MakeMemberAccess(paramExp, property);
                for (int i = 1; i < childProperties.Length; i++)
                {
                    property = property.PropertyType.GetProperty(childProperties[i]);
                    propExpression = Expression.MakeMemberAccess(propExpression, property);
                }
                propType = propExpression.Type;
                propExpression = Expression.Block(new[] { paramExp }, new[]{ propExpression });

            }
            else
            {
                propExpression = MemberExpression.Property(param, r.MemberName);
                propType = propExpression.Type;
            }

            // is the operator a known .NET operator?
            if (ExpressionType.TryParse(r.Operator, out tBinary))
            {
                var right = Expression.Constant(Convert.ChangeType(r.TargetValue, propType));
                // use a binary operation, e.g. 'Equal' -> 'u.Age == 15'
                return Expression.MakeBinary(tBinary, propExpression, right);
            }
            else
            {
                var method = propType.GetMethod(r.Operator);
                var tParam = method.GetParameters()[0].ParameterType;
                var right = Expression.Constant(Convert.ChangeType(r.TargetValue, tParam));
                // use a method call, e.g. 'Contains' -> 'u.Tags.Contains(some_tag)'
                return Expression.Call(propExpression, method, right);
            }
        }

    }
    public class Rule
    {
        public string MemberName { get; set; }
        public string Operator { get; set; }
        public string TargetValue { get; set; }
    }
}

,这是失败的测试

[TestMethod]
public void ChildPropertyRuleTest()
{
    Container container = new Container()
    {
        Repository = "TestRepo",
        Shipment = new Shipment() { OrderNumber = "555" }
    };

    MicroRuleEngine mr = new MicroRuleEngine();
    var rules = new List<Rule>() { new Rule() { MemberName = "Shipment.OrderNumber", Operator = "Contains", TargetValue = "55" } };
    var pases = mr.PassesRules<Container>(rules, container);
    Assert.IsTrue(!pases);
}

So I am building a MicroRuleEngine (Would love to see this take off as an OpenSource project) and I am running into a null reference Error When executing the compiled ExpressionTree and I am not exactly sure why. Rules against the simple properties work but going against Child Properties aka Customer.Client.Address.StreetName etc. do not work.

Below is the MicroRuleEngine

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace Trial
{
    public class MicroRuleEngine
    {
        public bool PassesRules<T>(List<Rule> rules, T toInspect)
        {
            bool pass = true;
            foreach (var rule in rules)
            {
                var cr = this.CompileRule<T>(rule);
                pass = pass && cr.Invoke(toInspect);
                if (!pass)
                    return pass;
            }
            return pass;
        }
        public Func<T, bool> CompileRule<T>(Rule r)
        {
            var paramUser = Expression.Parameter(typeof(T));
            Expression expr = BuildExpr<T>(r, paramUser);
            // build a lambda function User->bool and compile it

            return Expression.Lambda<Func<T, bool>>(expr, paramUser).Compile();
        }

        Expression BuildExpr<T>(Rule r, ParameterExpression param)
        {
            Expression propExpression;
            Type propType;// typeof(T).GetProperty(r.MemberName).PropertyType;
            ExpressionType tBinary;
            if (r.MemberName.Contains('.'))
            {
                // support to be sorted on child fields.
                String[] childProperties = r.MemberName.Split('.');
                var property = typeof(T).GetProperty(childProperties[0]);
                var paramExp = Expression.Parameter(typeof(T), "SomeObject");
                propExpression = Expression.MakeMemberAccess(paramExp, property);
                for (int i = 1; i < childProperties.Length; i++)
                {
                    property = property.PropertyType.GetProperty(childProperties[i]);
                    propExpression = Expression.MakeMemberAccess(propExpression, property);
                }
                propType = propExpression.Type;
                propExpression = Expression.Block(new[] { paramExp }, new[]{ propExpression });

            }
            else
            {
                propExpression = MemberExpression.Property(param, r.MemberName);
                propType = propExpression.Type;
            }

            // is the operator a known .NET operator?
            if (ExpressionType.TryParse(r.Operator, out tBinary))
            {
                var right = Expression.Constant(Convert.ChangeType(r.TargetValue, propType));
                // use a binary operation, e.g. 'Equal' -> 'u.Age == 15'
                return Expression.MakeBinary(tBinary, propExpression, right);
            }
            else
            {
                var method = propType.GetMethod(r.Operator);
                var tParam = method.GetParameters()[0].ParameterType;
                var right = Expression.Constant(Convert.ChangeType(r.TargetValue, tParam));
                // use a method call, e.g. 'Contains' -> 'u.Tags.Contains(some_tag)'
                return Expression.Call(propExpression, method, right);
            }
        }

    }
    public class Rule
    {
        public string MemberName { get; set; }
        public string Operator { get; set; }
        public string TargetValue { get; set; }
    }
}

And This is the Test that is Failing

[TestMethod]
public void ChildPropertyRuleTest()
{
    Container container = new Container()
    {
        Repository = "TestRepo",
        Shipment = new Shipment() { OrderNumber = "555" }
    };

    MicroRuleEngine mr = new MicroRuleEngine();
    var rules = new List<Rule>() { new Rule() { MemberName = "Shipment.OrderNumber", Operator = "Contains", TargetValue = "55" } };
    var pases = mr.PassesRules<Container>(rules, container);
    Assert.IsTrue(!pases);
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

梦里°也失望 2024-12-10 10:16:06

不认为您已经看过动态表达式解析器,它作为 VS2008 的示例项目捆绑在一起样本。它包括一个名为 ExpressionParser 的类型,可用于将字符串表达式转换为 Expression 实例。我之前使用过它来将字符串表达式转换为可编译委托,例如,我可以执行以下操作:

string expression = "(1 + 2)";
var func = FunctionFactory.Create<int>(expression);

int result = func(1, 2); // Result should be 3.

...其中 FunctionFactoryExpressionParser 类型的包装器。
我还可以做:

expression = "(a * b)";
var func2 = FunctionFactory.Create<int, int, int>(expresion new[] { "a", "b" });

int result = func2(10, 50); // Result should be 500;

或者一些有形的事情:

expression = "(Age == 5)";
var func3 = FunctionFactory.Create<Person, bool>(expression);

bool isFive = func3(new Person { Age = 5 });

这对你有用吗?您可以在此处阅读我的博客文章

Don't supposed you've had a look at the dynamic expression parser which was bundled as an example project for the VS2008 samples. It includes a type called an ExpressionParser which can be used to convert string expressions to Expression instances. I've used this previously to turn string expressions into compilable delegates, e.g, I could do something like:

string expression = "(1 + 2)";
var func = FunctionFactory.Create<int>(expression);

int result = func(1, 2); // Result should be 3.

...where FunctionFactory is a wrapper around the ExpressionParser type.
I could also do:

expression = "(a * b)";
var func2 = FunctionFactory.Create<int, int, int>(expresion new[] { "a", "b" });

int result = func2(10, 50); // Result should be 500;

Or something a little tangible:

expression = "(Age == 5)";
var func3 = FunctionFactory.Create<Person, bool>(expression);

bool isFive = func3(new Person { Age = 5 });

Would this be of any use to you? You can read my blog article here.

妖妓 2024-12-10 10:16:06

因此,我遇到的错误是我在试图找出如何访问子属性时阅读的所有示例都使用 MemberAccess Expressions 来遍历属性,并且我发现使用 PropertyExpressions 对于我的简单测试来说没有问题。以下是目前正在运行的更新

public class MicroRuleEngine
    {
        public bool PassesRules<T>(List<Rule> rules, T toInspect)
        {
            return this.CompileRules<T>(rules).Invoke(toInspect);
        }
        public Func<T, bool> CompileRule<T>(Rule r)
        {
            var paramUser = Expression.Parameter(typeof(T));
            Expression expr = BuildExpr<T>(r, paramUser);

            return Expression.Lambda<Func<T, bool>>(expr, paramUser).Compile();
        }

        public Func<T, bool> CompileRules<T>(IList<Rule> rules)
        {
            var paramUser = Expression.Parameter(typeof(T));
            List<Expression> expressions = new List<Expression>();
            foreach (var r in rules)
            {
                expressions.Add(BuildExpr<T>(r, paramUser));
            }
            var expr = AndExpressions(expressions);

            return Expression.Lambda<Func<T, bool>>(expr, paramUser).Compile();
        }

        Expression AndExpressions(IList<Expression> expressions)
        {
            if(expressions.Count == 1)
                return expressions[0];
            Expression exp = Expression.And(expressions[0], expressions[1]);
            for(int i = 2; expressions.Count > i; i++)
            {
                exp = Expression.And(exp, expressions[i]);
            }
            return exp;
        }

        Expression BuildExpr<T>(Rule r, ParameterExpression param)
        {
            Expression propExpression;
            Type propType;
            ExpressionType tBinary;
            if (r.MemberName.Contains('.'))
            {
                String[] childProperties = r.MemberName.Split('.');
                var property = typeof(T).GetProperty(childProperties[0]);
                var paramExp = Expression.Parameter(typeof(T), "SomeObject");

                propExpression = Expression.PropertyOrField(param, childProperties[0]);
                for (int i = 1; i < childProperties.Length; i++)
                {
                    property = property.PropertyType.GetProperty(childProperties[i]);
                    propExpression = Expression.PropertyOrField(propExpression, childProperties[i]);
                }
                propType = propExpression.Type;
            }
            else
            {
                propExpression = Expression.PropertyOrField(param, r.MemberName);
                propType = propExpression.Type;
            }

            // is the operator a known .NET operator?
            if (ExpressionType.TryParse(r.Operator, out tBinary))
            {
                var right = Expression.Constant(Convert.ChangeType(r.TargetValue, propType));
                // use a binary operation, e.g. 'Equal' -> 'u.Age == 15'
                return Expression.MakeBinary(tBinary, propExpression, right);
            }
            else
            {
                var method = propType.GetMethod(r.Operator);
                var tParam = method.GetParameters()[0].ParameterType;
                var right = Expression.Constant(Convert.ChangeType(r.TargetValue, tParam));
                // use a method call, e.g. 'Contains' -> 'u.Tags.Contains(some_tag)'
                return Expression.Call(propExpression, method, right);
            }
        }

    }
    public class Rule
    {
        public string MemberName { get; set; }
        public string Operator { get; set; }
        public string TargetValue { get; set; }
    }

So the error I was running into was all the examples I read in trying to find out how to access sub properties were using MemberAccess Expressions to walk down the properties and I found that using PropertyExpressions worked without a problem for the simple tests I have. Below is an update that is now working

public class MicroRuleEngine
    {
        public bool PassesRules<T>(List<Rule> rules, T toInspect)
        {
            return this.CompileRules<T>(rules).Invoke(toInspect);
        }
        public Func<T, bool> CompileRule<T>(Rule r)
        {
            var paramUser = Expression.Parameter(typeof(T));
            Expression expr = BuildExpr<T>(r, paramUser);

            return Expression.Lambda<Func<T, bool>>(expr, paramUser).Compile();
        }

        public Func<T, bool> CompileRules<T>(IList<Rule> rules)
        {
            var paramUser = Expression.Parameter(typeof(T));
            List<Expression> expressions = new List<Expression>();
            foreach (var r in rules)
            {
                expressions.Add(BuildExpr<T>(r, paramUser));
            }
            var expr = AndExpressions(expressions);

            return Expression.Lambda<Func<T, bool>>(expr, paramUser).Compile();
        }

        Expression AndExpressions(IList<Expression> expressions)
        {
            if(expressions.Count == 1)
                return expressions[0];
            Expression exp = Expression.And(expressions[0], expressions[1]);
            for(int i = 2; expressions.Count > i; i++)
            {
                exp = Expression.And(exp, expressions[i]);
            }
            return exp;
        }

        Expression BuildExpr<T>(Rule r, ParameterExpression param)
        {
            Expression propExpression;
            Type propType;
            ExpressionType tBinary;
            if (r.MemberName.Contains('.'))
            {
                String[] childProperties = r.MemberName.Split('.');
                var property = typeof(T).GetProperty(childProperties[0]);
                var paramExp = Expression.Parameter(typeof(T), "SomeObject");

                propExpression = Expression.PropertyOrField(param, childProperties[0]);
                for (int i = 1; i < childProperties.Length; i++)
                {
                    property = property.PropertyType.GetProperty(childProperties[i]);
                    propExpression = Expression.PropertyOrField(propExpression, childProperties[i]);
                }
                propType = propExpression.Type;
            }
            else
            {
                propExpression = Expression.PropertyOrField(param, r.MemberName);
                propType = propExpression.Type;
            }

            // is the operator a known .NET operator?
            if (ExpressionType.TryParse(r.Operator, out tBinary))
            {
                var right = Expression.Constant(Convert.ChangeType(r.TargetValue, propType));
                // use a binary operation, e.g. 'Equal' -> 'u.Age == 15'
                return Expression.MakeBinary(tBinary, propExpression, right);
            }
            else
            {
                var method = propType.GetMethod(r.Operator);
                var tParam = method.GetParameters()[0].ParameterType;
                var right = Expression.Constant(Convert.ChangeType(r.TargetValue, tParam));
                // use a method call, e.g. 'Contains' -> 'u.Tags.Contains(some_tag)'
                return Expression.Call(propExpression, method, right);
            }
        }

    }
    public class Rule
    {
        public string MemberName { get; set; }
        public string Operator { get; set; }
        public string TargetValue { get; set; }
    }
三岁铭 2024-12-10 10:16:06

在您的测试中,是否有可能您的容器中的 Shipment 属性尚未初始化?

其他一些建议:如果必须使用表达式,请考虑缓存表达式的编译版本,以便可以重用它,而不必在每次要使用它时重新编译它。

其次,您选择在规则中使用表达式而不是仅使用 Func 是否有特定原因?通常,在创建这样的规则引擎时,我的规则类定义为:

public class Rule
{
   public string Description {get; set;}
   public Func<T, bool> RuleToApply {get; set;}
}

鉴于此,我按如下方式实例化我的规则集合:

 var rules = new List<Rule>() { 
     new Rule { Description = "OrderNumber Contains 55", 
                RuleToApply = order => order.OrderNumber.Contains("55") } 
 }; 

并且 PassesRule 变为:

public bool PassesRules<T>(List<Rule> rules, T toInspect) 
{ 
    return rules.All(rule => rule(toInspect));
} 

这里的另一个优点是,我不传递字符串和评估表达式,而是保留类型通过使用字符串和动态构建表达式,我将失去安全和重构支持。

如果您正在构建可重用的表达式解析器,请记住另一件事,请确保在 VB 和 C# 中设置测试,因为它们并不总是在幕后生成相同的表达式树。特别是,添加字符串相等性的 VB 测试(city = "London")。我见过无数的 LINQ 提供商都忽略了这个简单的情况。

Is it possible in your test that the Shipment property has not been initialized in your container?

A couple other recommendations: If you must use expressions, consider caching the compiled version of the expression so that it can be reused rather than having to recompile it each time you want to use it.

Second, is there a specific reason you elected to go with expressions rather than just using Func in your rule? Typically when creating rules engines like this, my rule class is defined as somthing like:

public class Rule
{
   public string Description {get; set;}
   public Func<T, bool> RuleToApply {get; set;}
}

Given that, I instantiate my Rules collection as follows:

 var rules = new List<Rule>() { 
     new Rule { Description = "OrderNumber Contains 55", 
                RuleToApply = order => order.OrderNumber.Contains("55") } 
 }; 

And PassesRule becomes:

public bool PassesRules<T>(List<Rule> rules, T toInspect) 
{ 
    return rules.All(rule => rule(toInspect));
} 

The other advantage here is that instead of passing strings and evaulating expressions, I keep the type safety and refactoring support that I would loose by using strings and dynamically building expressions.

Another thing to keep in mind if you are building a reusable expression parser, make sure to set up tests in VB as well as C# because they don't always generate the same expression tree under the covers. In particular, add VB tests for string equality (city = "London"). I've seen countless LINQ providers which have overlooked this simple case.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文