• C#动态构建表达式树(三)——表达式的组合


    C#动态构建表达式树(三)——表达式的组合

    前言

    在筛选数据的过程中,可能会有这样的情况:有一些查询条件是公共的,但是根据具体的传入参数可能需要再额外增加一个条件。对于这种问题一般有两种方法:

    a. 在 Where 后再组合一个 Where,如:

    List<SOME_CLASS> dataList = dataList.Where(FILTER_1).Where(FILTER_2).ToList();
    

    b. 将类型相同两个表达式组合起来(就是本文的主题了)

    由于项目中既有框架的封装,查询时只能传入 Expression<Func<T, bool>> 类型的值,因此只能采用 b 方法。

    最终

    1.代码

    先给出研究后最后得出的方法,再分享自己踩坑的过程(仅以 And 操作为例,其它的大同小异)。

    public static Expression<Func<T, bool>> CombineLambda<T>(params Expression<Func<T, bool>>[] lambdas)
    {
        UnifyExpressionVisitor ueVisitor = new UnifyExpressionVisitor();
        ParameterExpression rootPe = ueVisitor.AddExpression(typeof(T));
    
        // 新建一个原始条件 a => true,并逐个组合数组中的条件
        Expression<Func<T, bool>> result = Expression.Lambda<Func<T, bool>>(
            Expression.Constant(true), rootPe);
        for (int i = 0; i < lambdas.Length; i++)
        {
            Expression tmp = ueVisitor.Visit(result.Body);
            Expression tmp1 = ueVisitor.Visit(lambdas[i].Body);
            result = Expression.Lambda<Func<T, bool>>(Expression.AndAlso(tmp, tmp1), rootPe);
        }
        return result;
    }
    
    private class UnifyExpressionVisitor : ExpressionVisitor
    {
        public List<ParameterExpression> PeList = new List<ParameterExpression>();
        public UnifyExpressionVisitor() { }
        
        public ParameterExpression AddExpression(Type type)
        {
            // 从 'A' 开始,为每个类型指定一个形参
            var result = Expression.Parameter(type, ((char)(65 + PeList.Count)).ToString());
            PeList.Add(result);
            return result;
        }
    
        // 覆写 VisitParameter 方法,返回我们指定的统一 Parameter
        protected override Expression VisitParameter(ParameterExpression p)
        {
            ParameterExpression peTmp = PeList.Find(a => a.Type == p.Type);
            if(peTmp == null)
            {
                return AddExpression(p.Type);
            }
            else
            {
                return peTmp;
            }                
        }
        public override Expression Visit(Expression node)
        {
            return base.Visit(node);
        }
    }
    

    Lambda表达式由参数(Parameter)内容(Body)组成。主要的思路是 将给出的所有 Lambda 表达式的 参数(Parameter) 都改为 同一个参数(Parameter),再把内容(Body)组合起来

    2. 验证

    假设我们有这样的数据:

    [
        {
            "Name": "安柏",
            "Age": 25,
            "Weapons": [
                {
                    "Name": "普通弓",
                    "GetTime": "2021-05-03T12:00:00+08:00",
                    "Description": "入门级武器,从无到有"
                }
            ]
        },
        {
            "Name": "行秋",
            "Age": 18,
            "Weapons": [
                {
                    "Name": "单手剑",
                    "GetTime": "2021-05-22T11:00:00+08:00",
                    "Description": "刚刚够用的武器"
                }
            ]
        },
        {
            "Name": "可莉",
            "Age": 8,
            "Weapons": [
                {
                    "Name": "嘟嘟可故事集",
                    "GetTime": "2021-06-20T15:35:00+08:00",
                    "Description": "特别合适的武器"
                },
                {
                    "Name": "简单的书",
                    "GetTime": "2021-05-03T16:47:00+08:00",
                    "Description": "赠送的武器"
                }
            ]
        }
    ]
    

    (原来你也玩原船,,,哦不是,原神啊)。主要有 “角色”“年龄”(我瞎编的)、“武器”(半真半编的)这几个字段,其中 “武器”是单独一个类,定义如下:

    /// <summary>
    /// 原神角色
    /// </summary>
    public class YuanshenRole
    {
        /// <summary>
        /// 名字
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 年龄
        /// </summary>
        public int Age { get; set; }
        /// <summary>
        /// 所拥有的武器
        /// </summary>
        public List<Weapon> Weapons { get; set; }
    }
    
    /// <summary>
    /// 武器
    /// </summary>
    public class Weapon
    {
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 获得的时间
        /// </summary>
        public DateTime GetTime { get; set; }
        /// <summary>
        /// 描述
        /// </summary>
        public string Description { get; set; }
    }
    

    我们要查找 Age 为 8, Name 为“可莉”,Weapons中存在 “嘟嘟可故事集” 的角色,分成两个表达式写,并组合起来:

    // 三个单独的条件
    Expression<Func<YuanshenRole, bool>> filter2 = x => x.Age == 8;
    Expression<Func<YuanshenRole, bool>> filter1 = x => x.Name == 可莉";
    Expression<Func<YuanshenRole, bool>> filter = x => x.Weapons.Any(y => y.Name == "嘟嘟可故事集");
    
    // 组合起来
    var finalFilter = CombineLambda(filter, filter1, filter2);
    
    

    CombineLambda 方法需要传入要组合的表达式。

    踩坑过程

    1. 只拼接Lambda表达式的Body,不改变其参数

    说起拼接,我的第一想法是这样的:

    // 错误的写法
    Expression<Func<YuanshenRole, bool>> finalFilter = Expression.Lambda<Func<YuanshenRole, bool>>(
        Expression.AndAlso(filter.Body, filter1.Body),    // 即使取了表达式的 Body,意义也不对
        Expression.Parameter(typeof(YuanshenRole), "x")
    );
    

    报错为:

    从作用域“”引用了“UsageDemo.Program+YuanshenRole”类型的变量“x”,但该变量未定义”

    在查找资料的时候看到了一个比喻:每个 Lambda 表达式就像是一个独轮车,把它们拼起来就相当于把两个独轮车拼成一个自行车。而上述的过程,我们看似把两个轮子拆下来,安装在了同一个车架子上,却没有改造两个轮子让其适应新的车架子(即,改变 “原有表达式 Body” 中引用 “原来Parameter” 的部分)

    自然而然地,我们需要进行一番深入的改造。搜索网上的资料后发现,可以通过继承 ExpressionVisitor 类并覆写其中的部分方法,如下:

    class MyExpressionVisitor : ExpressionVisitor
    {
        public ParameterExpression _Parameter0 { get; set; }
        public MyExpressionVisitor(ParameterExpression Parameter0)
        {
            _Parameter0 = Parameter0;
        }
        protected override Expression VisitParameter(ParameterExpression p)
        {
            return _Parameter0;
        }
        public override Expression Visit(Expression node)
        {
            return base.Visit(node);
        }
    }
    

    VisitParameter 方法在 Visit 时默认返回原 Parameter,通过覆写它可以达到改造 Body 中引用 “原来Parameter” 的效果

    ExpressionVisitor 中的 Visit 方法感觉与常规方法思路完全不同,先挖个坑,以后再研究

    顺理成章地,踩了第二个坑QAQ

    2. 只传入了 1 个 Parameter,而实际需要 2 个

    根据踩坑1,写法应该为:

    Expression<Func<YuanshenRole, bool>> filter = x => x.Weapons.Any(y => y.Name == "嘟嘟可故事集");
    Expression<Func<YuanshenRole, bool>> filter2 = x => x.Age == 8;
    
    
    ParameterExpression pe = Expression.Parameter(typeof(YuanshenRole), "x");
    var visitor1 = new MyExpressionVisitor(pe);            
    Expression bodyone1 = visitor1.Visit(filter.Body);    // 此处需要调用我们自定类的 Visit 方法改造原 Parameter
    Expression bodytwo1 = visitor1.Visit(filter2.Body);
    Expression<Func<YuanshenRole, bool>> finalFilter = 
        ExpressionLambda<Func<YuanshenRole, bool>>(Expression.AndAlso(bodyone1,bodytwo1), pe);
    dataList = dataList.AsQueryable().Where(finalFilter).ToList();
    

    结果报错为:

    没有为类型“UsageDemo.Program+YuanshenRole”定义属性“System.String Name””

    发生甚么事了,我们的 filter 也没有获取 YuanshenRole 的 Name 属性啊。此处我确实愣了一下,然后发现其实 查找的是 Weapon 类型的 Name,而传入的是 YuanshenRole 类型的参数

    因此在覆写的 Visit 方法中不能只返回一个类型,而要根据实际情况返回。稍加封装就有了文本最开始的代码。

    后记

    最一开始我觉得合并两个表达式应该是个很简单的操作,可能一个方法就搞定了,没想到他不讲武德,让我搞了这么长时间。我大 E 了啊,没有闪。希望这些代码以后耗子喂汁,不要再搞这样的聪明,小聪明啊,谢谢朋友们!

    最新

    2022-01-03

    最近工作中向同事介绍这个方法时,发现了原代码中大量冗余的逻辑,调用起来极不优雅,因此进行了修正。果然代码还是要讲一遍才更能发现其中的问题。

    参考

    LINQ系列(7)——表达式树之EXPRESSIONVISITOR

    合并两个 Lambda 表达式(此文中还介绍了通过 Invoke 方法来达到上述目的,但不适用于 IQueryable 类型的操作)

    C#中合并两个lambda表达式

    C#中利用Expression表达式树进行多个Lambda表达式合并

    C# 知识回顾 - 表达式树 Expression Trees

  • 相关阅读:
    co模块总结
    Promise总结
    webpack错误Chunk.entry was removed. Use hasRuntime()
    jquery validate用法总结
    node命令行开发
    animation总结
    formData使用总结
    vue-resource发送multipart/form-data数据
    keil中使用Astyle格式化你的代码的方法-keil4 keil5通用
    tcpip入门的网络教程汇总
  • 原文地址:https://www.cnblogs.com/battor/p/expression_combine_new.html
Copyright © 2020-2023  润新知