13ExpressionTree应用场景二
EF框架类似功能:SQL语句解析(条件)
表达式与EF框架
在我们使用EF框架进行筛选等等条件操作或者访问数据库的时候,EF都会将我们传的lambda表达式拼接为相应的表达式树,然后转为数据库认识的SQL语句格式的表达式如:
//EF上下文对象
StudentDBEntities studentDBEntities = new StudentDBEntities();
var query = studentDBEntities.Student.Where(s => s.Id > 1 && s.Address.Contains("门"));
Console.WriteLine(query);
输出结果如下:
实际上EF框架里的where方法也是需要一个表达式树作为参数如:
我们可以创建一个它所需要的表达式树并且条件和where里的一样:
Expression<Func<Student, bool>> expression = s => s.Id > 1 && s.Address.Contains("门");
我们输出可以看到,它就是一个lambda表达式
所以,我们可以通过ExpressionVisitor类里的方法手动的给它拼接成EF框架做的功能,EF框架原理应该也是通过这个类进行重写里面的方法进行拼接的。
我们如果要自己修改表达式树,以此来实现生成我们指定的特殊sql语句表达式,那么,我们可以了解一下ExpressionVisitor类,该类就是用来拼接查询用的表达式的。
ExpressionVisitor类
我认为:这个类用来将传入的表达式树自动拼接为表达式树,所以我们可以在该类的拼接表达式的对应方法里将方法重写,从而实现自己手动拼接复杂的sql条件的表达式,默认这类里面都是很多虚方法,它会将你传的lambda表达式拼接成默认的表达式树,我们
查看该类的内部方法
可以看到,这个类里大部分方法都是虚方法,而且这个类里的Visit方法就是根据我们传的表达式去调用对应的方法进行拼接表达式。
于是我们写一个Customevisitor类,继承ExpressionVisitor类,进行测试
public class Customevisitor: ExpressionVisitor
{
//自动根据传入的表达式调用指定方法进行拼接表达式,它递归的方式调用其它方法的,因为传入的表达式可能要通过多个方法才能进行拼接
public override Expression Visit(Expression node)
{
return base.Visit(node);
}
//遇到二元表达式会该方法拼接
protected override Expression VisitBinary(BinaryExpression node)
{
return base.VisitBinary(node);
}
//遇到方法表达式会该方法拼接
protected override Expression VisitMethodCall(MethodCallExpression node)
{
return base.VisitMethodCall(node);
}
//遇到常量表达式会该方法拼接
protected override Expression VisitConstant(ConstantExpression node)
{
return base.VisitConstant(node);
}
}
然后我们写一个表达式,再调用该类,把它里面的几个方法先重写一下,然后在Visit方法处打个断点,可以发现,我们表达式里有常量表达式和二元表达式和方法表达式,它都会在Visit方法里进行递归调用对应的方法进行拼接表达式树
我们可以输出结果,发现,虽然它确实根据我们传入的表达式节点类型通过Visit方法去递归调用了其它对应的方法去拼接表达式树,但是貌似就和没拼接一样,是因为这些虚方法里面是没有实现的,就是为了方便我们自己写实现如:
通过修改表达式树,让它变成我们数据库可以识别的sql条件语句就像EF框架那样
我们创建一个ConditionVisitor转换类,让其继承ExpressionVisitor类,在里面重写Visit方法会调用的常用的几个方法,以便于实现像EF那样的转换sql类型的表达式树
/// <summary>
/// 条件访问者
/// </summary>
public class ConditionVisitor : ExpressionVisitor
{
//创建一个栈,用来暂时存修改好的表达式,表达式树的永久性:想修改一个表达式 树,就必须复制这个表达式,然后替换其中的节点来创建要给 新的表达式,可以先定义一个栈来存
private Stack<string> _stringStack = new Stack<string>();
//返回栈里面的条件字符串,通过这个方法将需要表达式格式返回回去
public string Condition()
{
//将栈数组转为字符串
string condition = string.Concat(this._stringStack.ToArray());
this._stringStack.Clear();//清空栈,确保使用时栈里面是空的
return condition;
}
//最后转换的表达式如下:
// WHERE ([Extent1].[Id] > 1) AND ([Extent1].[Address] LIKE '%门%')
//二元表达式
protected override Expression VisitBinary(BinaryExpression node)
{
//栈是先进后出,所以要先右括号,而且Visit解析 都是从右边节点开始往左边解析的
//任何表达式节点类型都可以分左右两部分,除了常量表达式节点
this._stringStack.Push(")");
base.Visit(node.Right);//先解析右边
//因为是BinaryExpression二元表达式类型,所以node.NodeType会返回对应的二元表达式运算符的对应英文如加就是Add
this._stringStack.Push(" " + node.NodeType.ToSqlOperator() + " ");
base.Visit(node.Left);//先解析右边
this._stringStack.Push(" (");
//return base.VisitBinary(node);//这里就不返回父类的VisitBinary了,因为我们在上面已经手动拼接好了,而且父类方法里可能没有实现,所以就是默认原格式返回
return node;//返回手动拼接好的节点即可
}
//成员表达式
protected override Expression VisitMember(MemberExpression node)
{
this._stringStack.Push("[" + node.Member.Name + "]");
//return base.VisitMember(node);
return node;
}
//常量表达式
protected override Expression VisitConstant(ConstantExpression node)
{
//判断常量节点的值的类型
Type type = node.Value.GetType();
if (type.Equals(typeof(string)) || type.Equals(typeof(DateTime)))//如果是字符或者日期,数据库里规定是需要加单引号
{
this._stringStack.Push("'" + node.Value + "'");//成员入栈不要有空格,如果有空格值就不对了,因为多了一个空字符串
}
else
{
this._stringStack.Push(node.ToString());
}
//return base.VisitConstant(node);
return node;
}
//方法表达式
protected override Expression VisitMethodCall(MethodCallExpression node)
{
this.Visit(node.Object);//左边,方法所在的对象,返回成员表达式--》跳转到VisitMember方法
this.Visit(node.Arguments[0]);//右边 :方法参数,返回常量表达式 -》跳转到VisitConstant方法
//获取入栈数据,为方法解析做参数(参数和字段名)
string right = this._stringStack.Pop();
string left = this._stringStack.Pop();
//最后转换的表达式如下:
// WHERE ([Extent1].[Id] > 1) AND ([Extent1].[Address] LIKE '%门%')
switch (node.Method.Name)
{
case "Contains":
this._stringStack.Push($"{left} LIKE N '%{right.Replace("'", "")}%'");
break;
case "Equals":
this._stringStack.Push($"{left}={right}");
break;
default://还有很多方法可以自己写
throw new NotSupportedException(node.NodeType + "不支持");
}
return node;
}
}
在上面类里的二元表达式方法里会用到一个工厂方法,用于将BinaryExpression的实例node.NodeType转为数据库对应的二元表达式关键字,所以我们创建一个静态类,直接给Expression写一个扩展方法即可
/// <summary>
/// 运算符转换类,相当于工厂,比如:=、&&====>and
/// </summary>
internal static class SqlHperator
{
internal static string ToSqlOperator(this ExpressionType type)
{
//判断二元表达式返回类型
switch (type)
{
case ExpressionType.And:
case ExpressionType.AndAlso:
return "AND";
break;
case ExpressionType.Or:
case ExpressionType.OrElse:
return "OR";
break;
case ExpressionType.Not:
return "Not";
break;
case ExpressionType.NotEqual:
return "<>";
break;
case ExpressionType.GreaterThan:
return ">";
break;
case ExpressionType.GreaterThanOrEqual:
return ">=";
break;
case ExpressionType.LessThan:
return "<";
break;
case ExpressionType.LessThanOrEqual:
return "<=";
break;
case ExpressionType.Equal:
return "=";
break;
default:
throw new Exception("不支持该方法");
}
}
}
我们来调用该类,然后把表达式树放进去测试
可以看到,已经转换为对应
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?