在项目开发中,根据用户的需求,一般来是,我们的查询表达式是固定的,新的查询需求都要通过代码的修改来实现。而对于不确定的查询条件,固定查询表达式的方式显然是行不通的。
针对固定查询表达式存在的问题,我们提出基于表达式目录树的解决方案,该解决方案能帮助我们实时自动构建任何需要的查询表达式,以应用对各种复杂的查询场景。
通过定义IQueryable的扩展方法,来实现查询表达式的实时构建,并且以保证构建的查询表达式同IQueryable已支持的EF查询或内存查询的兼容性,完整代码已经上传CSDN,需要的读者可以免费下载,如果下载失败请留言告诉我邮箱。
基于既定需求,我们的实现目标如下:
IQueryable的扩展方法定于如下:
针对不同复杂度的查询,我们定义了两个WhereEnhance的方法,两个方法接收的参数类型不同。
本文通过定义一个IQueryable的扩展方法WhereEnhance来实现上述查询表达式构建流程,该方法既可以用于内存数据的查询,也可以和EF配合在一起查询数据库中的数据。具体代码如下:
public static IQueryable<T> WhereEnhance<T> (this IQueryable<T> source, List<WhereCondition> conditions) {
ParameterExpression parameter = Expression.Parameter(typeof(T),"s");
Expression conditionsExpression = CreateExpressions<T>(conditions, parameter);
LambdaExpression whereCondition = Expression.Lambda<Func<T,bool>>(conditionsExpression,new ParameterExpression[]{
parameter});
Console.WriteLine("Lambda Expression: " + whereCondition);
MethodCallExpression whereCallExpression = Expression.Call(
typeof(Queryable),
"Where",
new Type[] {
source.ElementType },
source.Expression,
whereCondition);
return source.Provider.CreateQuery<T>(whereCallExpression) as IQueryable<T>;
}
private static Expression CreateExpressions<T>(List<WhereCondition> conditions, ParameterExpression parameter){
Expression conditionsExpression = Expression.And(Expression.Constant(true),Expression.Constant(true));
ConditionRelation relation = ConditionRelation.And;
for(var i = 0 ; i< conditions.Count; ++i){
MemberExpression property = Expression.Property(parameter, conditions[i].PropertyName);
PropertyType propertyType = CheckFieldType(property);
Expression whereCondition;
if (propertyType == PropertyType.String){
whereCondition = buildStringWhere<T>(conditions[i], parameter);
}else if (propertyType == PropertyType.Number
|| propertyType == PropertyType.DateTime
|| propertyType == PropertyType.Enum
|| propertyType == PropertyType.Bool){
whereCondition = buildNumberOrDateTimeWhere<T>(conditions[i], parameter);
}else{
throw new ArgumentException($"We cannot support the property type {property.Type} in {typeof(T)}. ");
}
if (i==0){
conditionsExpression = whereCondition;
relation = conditions[i].NextRelation;
}else{
conditionsExpression = Combine<T>(
conditionsExpression,
whereCondition,
relation
);
relation = conditions[i].NextRelation;
}
}
return conditionsExpression;
}
public static IQueryable<T> WhereEnhance<T> (this IQueryable<T> source, ConditionTreeNode root) {
ParameterExpression parameter = Expression.Parameter(typeof(T),"s");
Expression conditionsExpression = CreateExpressions<T>(root, parameter);
LambdaExpression whereCondition = Expression.Lambda<Func<T,bool>>(conditionsExpression,new ParameterExpression[]{
parameter});
Console.WriteLine("Lambda Expression: " + whereCondition);
MethodCallExpression whereCallExpression = Expression.Call(
typeof(Queryable),
"Where",
new Type[] {
source.ElementType },
source.Expression,
whereCondition
);
return source.Provider.CreateQuery<T>(whereCallExpression) as IQueryable<T>;
}
private static Expression CreateExpressions<T>(ConditionTreeNode node, ParameterExpression parameter){
if (node.Relation != ConditionRelation.None){
return Combine<T>(
CreateExpressions<T>(node.LeftNode, parameter),
CreateExpressions<T>(node.RightNode, parameter),
node.Relation
);
}
else{
MemberExpression property = Expression.Property(parameter, node.NodeValue.PropertyName);
PropertyType propertyType = CheckFieldType(property);
Expression whereCondition;
if (propertyType == PropertyType.String){
whereCondition = buildStringWhere<T>(node.NodeValue, parameter);
}else if (propertyType == PropertyType.Number || propertyType == PropertyType.DateTime || propertyType == PropertyType.Enum){
whereCondition = buildNumberOrDateTimeWhere<T>(node.NodeValue, parameter);
}else{
throw new ArgumentException($"We cannot support the property type {property.Type} in {typeof(T)}. ");
}
return whereCondition;
}
}
buildStringWhere方法用于构建字符串类型的查询表达式。构建目标:
s => s.Property.Method(keyword),其中:
private static Expression buildStringWhere<T>(WhereCondition condition, ParameterExpression parameter){
Dictionary<string,string> methodsMap = new Dictionary<string,string>(){
{
EQ, "Equals"},
{
"contain", "Contains"},
{
"beginwith", "StartsWith"},
{
"endwith", "EndsWith"},
};
MemberExpression property = Expression.Property(parameter, condition.PropertyName);
ConstantExpression rightValue = GetConstantExpression(condition.PropertyValue, property);
ConstantExpression optionValue = Expression.Constant(StringComparison.CurrentCultureIgnoreCase);
Expression methodCall = Expression.Call(
property,
typeof(string).GetMethod(methodsMap[condition.Action],
new Type[]{
typeof(string),
//typeof(StringComparison)
}), // method
new Expression[] // Work method's parameter
{
rightValue,
// optionValue
}
);
if (condition.Reversed){
methodCall = Expression.Not(methodCall);
}
return methodCall;
}
buildNumberOrDateTimeWhere方法用于构建数字,枚举和日期类型的查询表达式。构建目标:
s=> s.Property [>|<|=|<=|>=] keywork 其中:
private static Expression buildNumberOrDateTimeWhere<T>(WhereCondition condition, ParameterExpression parameter){
MemberExpression propert = Expression.Property(parameter, condition.PropertyName);
ConstantExpression rightValue = GetConstantExpression(condition.PropertyValue, property);
Expression whereCondition;
switch(condition.Action){
case GT:
whereCondition = Expression.GreaterThan(property, rightValue);
break;
case GE:
whereCondition = Expression.GreaterThanOrEqual(property, rightValue);
break;
case LT:
whereCondition = Expression.LessThan(property, rightValue);
break;
case LE:
whereCondition = Expression.LessThanOrEqual(property, rightValue);
break;
default:
whereCondition = Expression.Equal(property, rightValue);
break;
}
if (condition.Reversed){
whereCondition = Expression.Not(whereCondition);
}
return whereCondition;
}
本次开发的WhereEnhance方法,作为IQueryable的扩展方法,可以支持内存查询和配合EFCore,进行数据库的查询。本文以查询银行分行信息作为查询数据,进行查询。
由于内存数据查询和数据库查询使用的数据和测试结果完全相同,故本文只提供数据库查询的测试用例和结果。如果需要内存查询的相关数据,请参看完整代码。测试数据详见附录。
[Table ("t_branch")]
public class Branch{
[Key,DatabaseGenerated(DatabaseGeneratedOption.Identity), Column(Order=0)]
public int Id {
get;set;}
[Required, Column(Order=1)]
public string BranchName {
get;set;}
[Required, Column(Order=2)]
public string BranchManager {
get;set;}
[Required, Column(Order=3)]
public int BranchCode {
get;set;}
[Column (TypeName = "datetime", Order=4), Required]
public DateTime BuildDate {
get;set;}
[Required, Column(Order=5)]
public BranchStatus Status {
get;set;} = BranchStatus.Open;
[Required, Column(Order=6)]
public bool HasATM {
get;set;} = true;
[Timestamp, Column(Order=7)]
public byte[] RowVersion {
get; set; }
}
数据库建表语句和初始化数据请参看附录。
using(var context = new ExpressionTreeContext()){
WhereCondition condition = new WhereCondition(){
PropertyName = "BranchCode",
PropertyValue = "4",
Action = ActionType.lt.ToString(),
Reversed = false
};
List<WhereCondition> conditions = new List<WhereCondition>(){
condition,
};
var branches = await context.Branches
.WhereEnhance(conditions)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
using(var context = new ExpressionTreeContext()){
WhereCondition condition = new WhereCondition(){
PropertyName = "BranchName",
PropertyValue = "分行2",
Action = ActionType.endwith.ToString(),
Reversed = false
};
List<WhereCondition> conditions = new List<WhereCondition>(){
condition,
};
var branches = await context.Branches
.WhereEnhance(conditions)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
执行结果:
using(var context = new ExpressionTreeContext()){
WhereCondition condition = new WhereCondition(){
PropertyName = "BuildDate",
PropertyValue = "2016-10-04",
Action = ActionType.gt.ToString(),
Reversed = false
};
List<WhereCondition> conditions = new List<WhereCondition>(){
condition,
};
var branches = await context.Branches
.WhereEnhance(conditions)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
using(var context = new ExpressionTreeContext()){
WhereCondition condition = new WhereCondition(){
PropertyName = "Status",
PropertyValue = "1",
Action = ActionType.eq.ToString(),
Reversed = false
};
List<WhereCondition> conditions = new List<WhereCondition>(){
condition,
};
var branches = await context.Branches
.WhereEnhance(conditions)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
using(var context = new ExpressionTreeContext()){
WhereCondition condition = new WhereCondition(){
PropertyName = "HasATM",
PropertyValue = "false",
Action = ActionType.eq.ToString(),
Reversed = false
};
List<WhereCondition> conditions = new List<WhereCondition>(){
condition,
};
var branches = await context.Branches
.WhereEnhance(conditions)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
using(var context = new ExpressionTreeContext()){
WhereCondition condition = new WhereCondition(){
PropertyName = "Status",
PropertyValue = "2",
Action = ActionType.eq.ToString(),
Reversed = true
};
List<WhereCondition> conditions = new List<WhereCondition>(){
condition,
};
var branches = await context.Branches
.WhereEnhance(conditions)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
using(var context = new ExpressionTreeContext()){
WhereCondition condition = new WhereCondition(){
PropertyName = "BuildDate",
PropertyValue = "2016-10-04",
Action = ActionType.gt.ToString(),
Reversed = true
};
List<WhereCondition> conditions = new List<WhereCondition>(){
condition,
};
var branches = await context.Branches
.WhereEnhance(conditions)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
using(var context = new ExpressionTreeContext()){
WhereCondition conditionStartDate = new WhereCondition(){
PropertyName = "BuildDate",
PropertyValue = "2016-10-02",
Action = ActionType.gt.ToString(),
Reversed = false
};
WhereCondition conditionEndDate = new WhereCondition(){
PropertyName = "BuildDate",
PropertyValue = "2016-10-06",
Action = ActionType.le.ToString(),
Reversed = false
};
List<WhereCondition> conditions = new List<WhereCondition>(){
conditionStartDate,
conditionEndDate,
};
var branches = await context.Branches
.WhereEnhance(conditions)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
using(var context = new ExpressionTreeContext()){
WhereCondition condition = new WhereCondition(){
PropertyName = "Status",
PropertyValue = "1",
Action = ActionType.eq.ToString(),
Reversed = true
};
WhereCondition conditionStartDate = new WhereCondition(){
PropertyName = "BuildDate",
PropertyValue = "2016-10-02",
Action = ActionType.gt.ToString(),
Reversed = false
};
WhereCondition conditionEndDate = new WhereCondition(){
PropertyName = "BuildDate",
PropertyValue = "2016-10-07",
Action = ActionType.le.ToString(),
Reversed = false
};
List<WhereCondition> conditions = new List<WhereCondition>(){
conditionStartDate,
conditionEndDate,
condition,
};
var branches = await context.Branches
.WhereEnhance(conditions)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
using(var context = new ExpressionTreeContext()){
WhereCondition condition = new WhereCondition(){
PropertyName = "BranchManager",
PropertyValue = "Tom",
Action = ActionType.eq.ToString(),
Reversed = false
};
WhereCondition conditionStartDate = new WhereCondition(){
PropertyName = "BuildDate",
PropertyValue = "2016-10-02",
Action = ActionType.lt.ToString(),
NextRelation = ConditionRelation.Or,
Reversed = false
};
WhereCondition conditionEndDate = new WhereCondition(){
PropertyName = "BuildDate",
PropertyValue = "2016-10-04",
Action = ActionType.gt.ToString(),
Reversed = false
};
List<WhereCondition> conditions = new List<WhereCondition>(){
conditionStartDate,
conditionEndDate,
condition,
};
var branches = await context.Branches
.WhereEnhance(conditions)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
如果我们将执行顺序改为如下:
List<WhereCondition> conditions = new List<WhereCondition>(){
condition,
conditionStartDate,
conditionEndDate,
};
根据测试结果可以看到,如果改变条件的顺序,无论时生成的查询表达式,还是EFCore生成的SQL都不是我们预期的。由于当前版本不支持括号,请将Or操作尽量放到查询条件的最前面。
查询条件的Lambda表达式构建目标:
t => (([t].[BranchCode] > 1) AND ([t].[BranchCode] < 3)) OR ((([t].[BranchCode] < 9) AND ([t].[BranchCode] > 5))
AND ([t].[BranchManager] = N’Tom’))
using(var context = new ExpressionTreeContext()){
ConditionTreeNode root = new ConditionTreeNode();
root.Relation = ConditionRelation.Or;
ConditionTreeNode p11 = new ConditionTreeNode();
p11.Relation = ConditionRelation.And;
root.LeftNode = p11;
ConditionTreeNode p12 = new ConditionTreeNode();
p12.Relation = ConditionRelation.And;
root.RightNode = p12;
ConditionTreeNode p21 = new ConditionTreeNode();
p11.LeftNode = p21;
p21.NodeValue = new WhereCondition(){
PropertyName = "BranchCode",
Action = "gt",
PropertyValue = "1"
};
ConditionTreeNode p22 = new ConditionTreeNode();
p11.RightNode = p22;
p22.NodeValue = new WhereCondition(){
PropertyName = "BranchCode",
Action = "lt",
PropertyValue = "3"
};
ConditionTreeNode p23 = new ConditionTreeNode();
p23.Relation = ConditionRelation.And;
ConditionTreeNode p24 = new ConditionTreeNode();
p24.NodeValue = new WhereCondition(){
PropertyName = "BranchManager",
Action = "eq",
PropertyValue = "Tom",
Reversed = false
};
p12.LeftNode = p23;
p12.RightNode = p24;
ConditionTreeNode p31 = new ConditionTreeNode();
p31.NodeValue = new WhereCondition(){
PropertyName = "BranchCode",
Action = "lt",
PropertyValue = "9"
};
ConditionTreeNode p32 = new ConditionTreeNode();
p32.NodeValue = new WhereCondition(){
PropertyName = "BranchCode",
Action = "gt",
PropertyValue = "5"
};
p23.LeftNode = p31;
p23.RightNode = p32;
string json = JsonConvert.SerializeObject(root);
root = JsonConvert.DeserializeObject<ConditionTreeNode>(json);
var branches = await context.Branches
.WhereEnhance(root)
.AsNoTracking ()
.ToListAsync ();
foreach(var branch in branches){
System.Console.WriteLine(branch.BranchName);
}
}
public class WhereCondition{
public string PropertyName {
get;set;}
public string Action {
get;set;}
public bool Reversed {
get;set;} = false;
public string PropertyValue {
get;set;}
public ConditionRelation NextRelation {
get;set;} = ConditionRelation.And;
}
public enum ConditionRelation{
And= 1,
Or,
None
}
public class ConditionTreeNode{
public ConditionTreeNode LeftNode {
get;set;}
public ConditionTreeNode RightNode {
get;set;}
public ConditionRelation Relation {
get;set;} = ConditionRelation.None;
public WhereCondition NodeValue {
get;set;}
}
WhereCondition类具体内容包括:
ConditionTreeNode类具体内容包括:
public class Branch {
public string BranchName {
get;set;}
public int BranchCode {
get;set;}
public DateTime BuildDate {
get;set;}
public BranchStatus Status {
get;set;} = BranchStatus.Open;
public string BranchManager {
get;set;}
}
public enum BranchType {
Open = 1,
Closed,
Maintance
}
public enum PropertyType {
String = 1,
Number,
DateTime,
Enum,
Bool,
Unsupported
}
private static PropertyType CheckFieldType(MemberExpression member){
string typeName = member.Type.ToString();
string baseTypeName = member.Type.BaseType.ToString();
string[] supportedNumbertypes = new string []{
DECIMAL, BYTE, SHORT , INT, LONG};
if (baseTypeName == ENUM){
return PropertyType.Enum;
}
if (typeName == STRING){
return PropertyType.String;
}else if (supportedNumbertypes.Contains(typeName)){
return PropertyType.Number;
}else if (typeName == DATETIME){
return PropertyType.DateTime;
} else if (typeName == BOOL){
return PropertyType.Bool;
}
return PropertyType.Unsupported;
}
private static ConstantExpression GetConstantExpression(string val, MemberExpression member){
ConstantExpression constExpression;
string typeName = member.Type.ToString();
string baseTypeName = member.Type.BaseType.ToString();
switch(typeName){
case STRING:
constExpression = Expression.Constant(val, member.Type);
break;
case DECIMAL:
constExpression = Expression.Constant(Convert.ToDecimal(val), member.Type);
break;
case SHORT:
constExpression = Expression.Constant(Convert.ToInt16(val), member.Type);
break;
case INT:
constExpression = Expression.Constant(Convert.ToInt32(val), member.Type);
break;
case LONG:
constExpression = Expression.Constant(Convert.ToInt64(val), member.Type);
break;
case DATETIME:
constExpression = Expression.Constant(DateTime.Parse(val), member.Type);
break;
case BYTE:
constExpression = Expression.Constant(Convert.ToByte(val), member.Type);
break;
case BOOL:
constExpression = Expression.Constant(Convert.ToBoolean(val), member.Type);
break;
default:
if (baseTypeName == ENUM){
int data = Int32.Parse(val.ToString());
String name = Enum.GetName(member.Type, data);
constExpression = Expression.Constant(Enum.Parse(member.Type, name, false), member.Type);
}else{
constExpression = Expression.Constant(Convert.ToInt64(val), member.Type);
}
break;
}
return constExpression;
}
}
注意枚举类型需要通过基类来判断。
private static Expression Combine<T>
(
Expression first,
Expression second,
ConditionRelation relation = ConditionRelation.And
)
{
if ( relation == ConditionRelation.And){
return Expression.AndAlso(first, second);
}else{
return Expression.OrElse(first, second);
}
}
数据库建表语句:
USE [Bank]
GO
/****** Object: Table [dbo].[t_branch] Script Date: 2021/2/1 16:38:49 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[t_branch](
[Id] [int] IDENTITY(1,1) NOT NULL,
[BranchName] [nvarchar](max) NOT NULL,
[BranchCode] [int] NOT NULL,
[BuildDate] [datetime] NOT NULL,
[RowVersion] [timestamp] NULL,
[BranchManager] [nvarchar](max) NOT NULL,
[HasATM] [bit] NOT NULL,
[Status] [int] NOT NULL,
CONSTRAINT [PK_t_branch] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[t_branch] ADD DEFAULT (N'') FOR [BranchManager]
GO
ALTER TABLE [dbo].[t_branch] ADD DEFAULT (CONVERT([bit],(0))) FOR [HasATM]
GO
ALTER TABLE [dbo].[t_branch] ADD DEFAULT ((0)) FOR [Status]
GO
数据初始化语句:
insert into [dbo].[t_branch] ([BranchName], [BranchCode], [BuildDate],[BranchManager], [HasATM], [Status] ) values
(N'天津分行1',1, '2016-10-01', N'Tom',1,1),
(N'天津分行2',2, '2016-10-02', N'Tom',1,1),
(N'天津分行3',3, '2016-10-03', N'Tom',1,1),
(N'天津分行4',4, '2016-10-04', N'Tom',1,1),
(N'天津分行5',5, '2016-10-05', N'Tom',1,1),
(N'天津分行6',6, '2016-10-06', N'Tom',0,1),
(N'天津分行7',7, '2016-10-07', N'Jack',1,2),
(N'天津分行8',8, '2016-10-08', N'Jack',1,2),
(N'天津分行9',9, '2016-10-09', N'Mary',1,3)
本次开发的WhereEnhance方法,让我们可以借助表达式目录树相关的类和方法,根据需要动态构建查询表达式,便于业务系统的扩展。但还存在一些不足,未来可以加强的功能包括: