C#表达式目录树示例详解

作者:一只独行的猿 时间:2021-10-01 01:52:46 

目录
  • 1、表达式目录树

  • 2、构建表达式目录树

  • 3、使用Expression来进行不同对象的相同名字的属性映射

  • 4、表达式目录树构建SQL删选

  •  5、修改表达式目录树

  • 6、构建模拟EF的表达式目录树解析

  • 7、连接表达式目录树

1、表达式目录树

表达式目录树,在C#中是Expression来定义的,它是一种语法树,或者说是一种数据结构。其主要用于存储需要计算、运算的一种结构,它只提供存储功能,不进行运算。通常Expression是配合Lambda一起使用,lambda可以是匿名方法。Expression可以动态创建。

声明一个lambda表达式,其中可以指明类型,也可以是匿名方法:


//Func<int, int, int> func = new Func<int, int, int>((m, n) => m * n + 2);
Func<int, int, int> func = (m, n) => m * n + 2;

上述代码可以使用Expression来定义:


Expression<Func<int, int, int>> exp = (m, n) => m * n + 2;//lambda表达式声明表达式目录树

Expression的方法体只能是一个整体,不能具有花括号,以下代码是不允许的:


Expression<Func<int, int, int>> exp1 = (m, n) =>//方法体只能一体
{
return m * n + 2;
};

上述func和exp执行结果相同:


int iResult1 = func.Invoke(3, 2);
int iResult2 = exp.Compile().Invoke(3, 2);

2、构建表达式目录树

上述表达式示例可以通过Expression来自主构建,把m、n定义为ParameterExpression参数,把2定义为常数表达式ConstantExpression,使用Expression的静态方法,表示乘和加:


ParameterExpression parameterLeft = Expression.Parameter(typeof(int), "m");//定义参数
ParameterExpression parameterRight = Expression.Parameter(typeof(int), "n");//定义参数
BinaryExpression binaryMultiply = Expression.Multiply(parameterLeft, parameterRight);//组建第一步的乘法  
ConstantExpression constant = Expression.Constant(2, typeof(int)); //定义常数参数
BinaryExpression binaryAdd = Expression.Add(binaryMultiply, constant);//组建第二步的加法
var expression = Expression.Lambda<Func<int, int, int>>(binaryAdd, parameterLeft, parameterRight);//构建表达式
var func = expression.Compile(); //编译为lambda表达式
int iResult3 = func(3, 2);
int iResult4 = expression.Compile().Invoke(3, 2);
int iResult5 = expression.Compile()(3, 2);

自主构建Expression是,参数名称的定义,可以不是m、n,可以是其他的a、b或者x、y。

如何构建一个复杂的表达式目录树?需要使用到Expression中更多的方法、属性、扩展方法等。首先定义一个类:


public class People
{
public int Age { get; set; }
public string Name { get; set; }
public int Id;
}

基于上面的类,构建表达式: Expression<Func<People, bool>> lambda = x => x.Id.ToString().Equals("5");

这个示例中,使用到了int自身的ToString()方法,还使用到了字符串的Equals方法。构建过程如下:


//以下表达式目录树实现lambda的表达式
Expression<Func<People, bool>> lambda = x => x.Id.ToString().Equals("5");
//声明一个参数对象
ParameterExpression parameterExpression = Expression.Parameter(typeof(People), "x");
//查找字段, 并绑定访问参数对象字段(属性)的方法:x.Id
MemberExpression member = Expression.Field(parameterExpression, typeof(People).GetField("Id"));
//以上可以用这个代替
var temp =Expression.PropertyOrField(parameterExpression, "Id");
//调用字段的ToString方法:x.Id.ToString()
MethodCallExpression method = Expression.Call(member, typeof(int).GetMethod("ToString", new Type[] { }), new Expression[0]);
//调用字符串的Equals方法:x.Id.ToString().Equals("5")
MethodCallExpression methodEquals = Expression.Call(method, typeof(string).GetMethod("Equals", new Type[] { typeof(string) }), new Expression[]
{
Expression.Constant("5", typeof(string))//与常量进行比较,也可以是参数
});
//创建目录树表达式
ar expression = Expression.Lambda<Func<People, bool>>(methodEquals, new ParameterExpression[] {parameterExpression });
bool bResult = expression.Compile().Invoke(new People()
{
Id = 5,
Name = "Nigle",
Age = 31
});

3、使用Expression来进行不同对象的相同名字的属性映射

前面构建了类People,现在我们构建一个新的类PeopleCopy:


public class PeopleCopy
{
public int Age { get; set; }
public string Name { get; set; }
public int Id;
}

现在声明一个People对象,然后对People对象的数据进行拷贝到PeopleCopy新对象中去,直接硬编码的方式:

1. 硬编码


People people = new People()
{
Id = 11,
Name = "Nigle",
Age = 31
};
PeopleCopy peopleCopy = new PeopleCopy()
{
Id = people.Id,
Name = people.Name,
Age = people.Age
};

如果这样编写,对于属性或者字段比较多的类,在拷贝时,我们需要编写很多次赋值,代码也会很长。此时,我们能想到的是通过反射的方式进行拷贝:

2. 反射拷贝


public static TOut Trans<TIn, TOut>(TIn tIn)
{
TOut tOut = Activator.CreateInstance<TOut>();
foreach (var itemOut in tOut.GetType().GetProperties())
{
foreach (var itemIn in tIn.GetType().GetProperties())
{
 if (itemOut.Name.Equals(itemIn.Name))
 {
 itemOut.SetValue(tOut, itemIn.GetValue(tIn));
 break;
 }
}
}
foreach (var itemOut in tOut.GetType().GetFields())
{
foreach (var itemIn in tIn.GetType().GetFields())
{
 if (itemOut.Name.Equals(itemIn.Name))
 {
 itemOut.SetValue(tOut, itemIn.GetValue(tIn));
 break;
 }
}
}
return tOut;
}

通过反射,我们可以通过输出类型的属性或者字段去查找原类型对应的属性和字段,然后获取值,并设置值的方式进行赋值拷贝。除此之外,我们还能想到的是深克隆的序列化方式,进行反序列化数据:

3. 序列化和反序列化


public class SerializeMapper
{
/// <summary>序列化反序列化方式/summary>
public static TOut Trans<TIn, TOut>(TIn tIn)
{
//采用的是json序列化,也可以采用其他序列化方式
return JsonConvert.DeserializeObject<TOut>(JsonConvert.SerializeObject(tIn));
}
}

前面的三种方法是最为常用的方法,但未使用到本文介绍的表达式目录树。如何将表达式目录树与拷贝结合起来?有两种方式【缓存+表达式目录】,【泛型+表达式目录】

4. 缓存+表达式目录


/// <summary>
/// 生成表达式目录树 缓存
/// </summary>
public class ExpressionMapper
{
private static Dictionary<string, object> _Dic = new Dictionary<string, object>();

/// <summary>
/// 字典缓存表达式树
/// </summary>
public static TOut Trans<TIn, TOut>(TIn tIn)
{
string key = string.Format("funckey_{0}_{1}", typeof(TIn).FullName, typeof(TOut).FullName);
if (!_Dic.ContainsKey(key))
{
 ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");
 List<MemberBinding> memberBindingList = new List<MemberBinding>();
 foreach (var item in typeof(TOut).GetProperties())
 {
 MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));
 //绑定Out和In之间的关系:Age = p.Age
 MemberBinding memberBinding = Expression.Bind(item, property);
 memberBindingList.Add(memberBinding);
 }
 foreach (var item in typeof(TOut).GetFields())
 {
 MemberExpression property = Expression.Field(parameterExpression, typeof(TIn).GetField(item.Name));
 MemberBinding memberBinding = Expression.Bind(item, property);
 memberBindingList.Add(memberBinding);
 }
 MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberBindingList.ToArray());
 Expression<Func<TIn, TOut>> lambda = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, parameterExpression);
 Func<TIn, TOut> func = lambda.Compile();//拼装是一次性的
 _Dic[key] = func;
}
return ((Func<TIn, TOut>)_Dic[key]).Invoke(tIn);
}
}

5. 泛型+表达式目录


/// <summary>
/// 生成表达式目录树 泛型缓存
/// </summary>
/// <typeparam name="TIn"></typeparam>
/// <typeparam name="TOut"></typeparam>
public class ExpressionGenericMapper<TIn, TOut>//Mapper`2
{
private static Func<TIn, TOut> func = null;
static ExpressionGenericMapper()
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");
List<MemberBinding> memberBindingList = new List<MemberBinding>();
foreach (var item in typeof(TOut).GetProperties())
{
 MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));
 MemberBinding memberBinding = Expression.Bind(item, property);
 memberBindingList.Add(memberBinding);
}
foreach (var item in typeof(TOut).GetFields())
{
 MemberExpression property = Expression.Field(parameterExpression, typeof(TIn).GetField(item.Name));
 MemberBinding memberBinding = Expression.Bind(item, property);
 memberBindingList.Add(memberBinding);
}
MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberBindingList.ToArray());
Expression<Func<TIn, TOut>> lambda = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, new ParameterExpression[]
{
 parameterExpression
});
func = lambda.Compile();//拼装是一次性的
}
public static TOut Trans(TIn t)
{
return func(t);
}
}

除了上述5中方法,还可以使用框架自带的AutoMapper,首先我们要nuget添加引用AutoMapper即可直接使用,具体代码为:

6. AutoMapper


public class AutoMapperTest
{
public static TOut Trans<TIn, TOut>(TIn tIn)
{
return AutoMapper.Mapper.Instance.Map<TOut>(tIn);
}
}

测评:对上述6种方式进行测评,每一种拷贝方式运行100 0000次:


Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 1000000; i++)
{
//测试六种方法
PeopleCopy peopleCopy = new PeopleCopy() {Id = people.Id, Name = people.Name,Age = people.Age}; //直接赋值的方式复制--22ms
//PeopleCopy peopleCopy = ReflectionMapper.Trans<People, PeopleCopy>(people);   //反射赋值的方式复制---1573ms
//PeopleCopy peopleCopy = SerializeMapper.Trans<People, PeopleCopy>(people);   //序列化方式---2716ms
//PeopleCopy peopleCopy = ExpressionMapper.Trans<People, PeopleCopy>(people);   //表达式目录树 缓存 复制---517ms
//PeopleCopy peopleCopy = ExpressionGenericMapper<People, PeopleCopy>.Trans(people);  //表达式目录树 泛型缓存--77ms
//PeopleCopy peopleCopy = AutoMapperTest.Trans<People, PeopleCopy>(people);    //AutoMapper---260ms
}
watch.Stop();
Console.WriteLine($"耗时:{ watch.ElapsedMilliseconds} ms");

4、表达式目录树构建SQL删选

传统的sql在构建条件语句时,需要通过诸多判断,进而构建成完整的查询语句。


People p = new People()
{
Id = 11,
Name = "Nigle",
Age = 31
};
//拼装sql的方式
string sql = "SELECT * FROM USER WHERE Id=1";
if (string.IsNullOrWhiteSpace(p.Name))
{
sql += $" and name like '%{p.Name}%'";
}
sql += $" and age >{p.Age}";

事实上,我们偶尔我们会使用linq查询或者lambda表达式,用于条件筛选,如var lambda = x => x.Age > 5; 事实上,我们可以构建上述Expression:


People p = new People()
{
Id = 11,
Name = "Nigle",
Age = 31
};
//拼装表达式目录树,交给下端用
ParameterExpression parameterExpression = Expression.Parameter(typeof(People), "x");//声明一个参数
Expression propertyExpression = Expression.Property(parameterExpression, typeof(People).GetProperty("Age"));//声明访问参数属性的对象
//Expression property = Expression.Field(parameterExpression, typeof(People).GetField("Id"));
ConstantExpression constantExpression = Expression.Constant(5, typeof(int));//声明一个常量
BinaryExpression binary = Expression.GreaterThan(propertyExpression, constantExpression);//添加比较方法
var lambda = Expression.Lambda<Func<People, bool>>(binary, new ParameterExpression[] { parameterExpression });//构建表达式主体
bool bResult = lambda.Compile().Invoke(p); //比较值

 5、修改表达式目录树

本示例将把已经构建完成的表达式目录树的加法进行修改为减法。修改、拼接、读取节点,需要使用到ExpressionVisitor类,ExpressionVisitor类能动态的解耦,读取相关的节点和方法。

ExpressionVisitor类中的Visit(Expression node)是解读表达式的入口,然后能够神奇的区分参数和方法体,然后将表达式调度到此类中更专用的访问方法中,然后一层一层的解析下去,直到最终的叶节点!

首先编写OperationsVisitor类,用于修改:


internal class OperationsVisitor : ExpressionVisitor
{
public Expression Modify(Expression expression)
{
return this.Visit(expression);
}
protected override Expression VisitBinary(BinaryExpression b)
{
if (b.NodeType == ExpressionType.Add)
{
 Expression left = this.Visit(b.Left);
 Expression right = this.Visit(b.Right);
 return Expression.Subtract(left, right);
}

return base.VisitBinary(b);
}
protected override Expression VisitConstant(ConstantExpression node)
{
return base.VisitConstant(node);
}
}

然后,编写lambda表达式,进行修改并计算结果:


//修改表达式目录树
Expression<Func<int, int, int>> exp = (m, n) => m * n + 2;
OperationsVisitor visitor = new OperationsVisitor();
Expression expNew = visitor.Modify(exp);

int? iResult = (expNew as Expression<Func<int, int, int>>)?.Compile().Invoke(2, 3);

Visit这个这个方法能够识别出来 m*n+2 是个二叉树,会通过下面的图然后一步一步的进行解析,如果遇到m*n 这会直接调用VisitBinary(BinaryExpression b)这个方法,如果遇到m或者n会调用VisitParameter(ParameterExpression node)这个方法,如果遇到2常量则会调用VisitConstant(ConstantExpression node)。

ORM与表达式树目录的关系:

经常用到EF,其实都是继承Queryable,然后我们使用的EF通常都会使用 var items = anserDo.GetAll().Where(x => x.OrganizationId == input.oid || input.oid == 0) ,where其实传的就是表达式目录树。EF写的where等lambda表达式,就是通过ExpressionVisitor这个类来反解析的!后面将构建模拟EF的解析方法。

6、构建模拟EF的表达式目录树解析

首先,构建解析表达式目录树的方法,不能再使用默认的。


/// <summary>
/// 表达式目录树中的访问者
/// </summary>
internal class ConditionBuilderVisitor : ExpressionVisitor
{
/// <summary>
/// 用于存放条件等数据
/// </summary>
private Stack<string> _StringStack = new Stack<string>();
/// <summary>
///
/// </summary>
/// <returns></returns>
internal string Condition()
{
string condition = string.Concat(this._StringStack.ToArray());
this._StringStack.Clear();
return condition;
}
/// <summary>
/// 如果是二元表达式
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
protected override Expression VisitBinary(BinaryExpression node)
{
if (node == null) throw new ArgumentNullException("BinaryExpression");

this._StringStack.Push(")");
base.Visit(node.Right);//解析右边
this._StringStack.Push(" " + ToSqlOperator(node.NodeType) + " ");
base.Visit(node.Left);//解析左边
this._StringStack.Push("(");

return node;
}
/// <summary>
///
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
protected override Expression VisitMember(MemberExpression node)
{
if (node == null)
 throw new ArgumentNullException("MemberExpression");
this._StringStack.Push(" [" + node.Member.Name + "] ");
return node;
return base.VisitMember(node);
}
/// <summary>
/// 将节点类型转换为Sql的操作符
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
string ToSqlOperator(ExpressionType type)
{
switch (type)
{
 case (ExpressionType.AndAlso):
 case (ExpressionType.And):
 return "AND";
 case (ExpressionType.OrElse):
 case (ExpressionType.Or):
 return "OR";
 case (ExpressionType.Not):
 return "NOT";
 case (ExpressionType.NotEqual):
 return "<>";
 case ExpressionType.GreaterThan:
 return ">";
 case ExpressionType.GreaterThanOrEqual:
 return ">=";
 case ExpressionType.LessThan:
 return "<";
 case ExpressionType.LessThanOrEqual:
 return "<=";
 case (ExpressionType.Equal):
 return "=";
 default:
 throw new Exception("不支持该方法");
}
}
/// <summary>
/// 常量表达式
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
protected override Expression VisitConstant(ConstantExpression node)
{
if (node == null)
 throw new ArgumentNullException("ConstantExpression");
this._StringStack.Push(" '" + node.Value + "' ");
return node;
}
/// <summary>
/// 方法表达式
/// </summary>
/// <param name="m"></param>
/// <returns></returns>
protected override Expression VisitMethodCall(MethodCallExpression m)
{
if (m == null) throw new ArgumentNullException("MethodCallExpression");

string format;
switch (m.Method.Name)
{
 case "StartsWith":
 format = "({0} LIKE {1}+'%')";
 break;

case "Contains":
 format = "({0} LIKE '%'+{1}+'%')";
 break;

case "EndsWith":
 format = "({0} LIKE '%'+{1})";
 break;

default:
 throw new NotSupportedException(m.NodeType + " is not supported!");
}
this.Visit(m.Object);
this.Visit(m.Arguments[0]);
string right = this._StringStack.Pop();
string left = this._StringStack.Pop();
this._StringStack.Push(String.Format(format, left, right));
return m;
}
}

然后,外部就可以通过编写表达式目录树的查询条件,再通过这个类的实例进行解析成对应的SQL语句:


{
Expression<Func<People, bool>> lambda = x => x.Age > 5 && x.Id > 5
     && x.Name.StartsWith("1")
     && x.Name.EndsWith("1")
     && x.Name.Contains("2");
//“ x => x.Age > 5 && x.Id > 5”等同于sql语句
string sql = string.Format("Delete From [{0}] WHERE {1}", typeof(People).Name, " [Age]>5 AND [ID] >5");
ConditionBuilderVisitor vistor = new ConditionBuilderVisitor();
vistor.Visit(lambda);
Console.WriteLine(vistor.Condition());
}
{
Expression<Func<People, bool>> lambda = x => x.Age > 5 && x.Name == "A" || x.Id > 5;
ConditionBuilderVisitor vistor = new ConditionBuilderVisitor();
vistor.Visit(lambda);
Console.WriteLine(vistor.Condition());
}
{
Expression<Func<People, bool>> lambda = x => x.Age > 5 || (x.Name == "A" && x.Id > 5);
ConditionBuilderVisitor vistor = new ConditionBuilderVisitor();
vistor.Visit(lambda);
Console.WriteLine(vistor.Condition());
}
{
Expression<Func<People, bool>> lambda = x => (x.Age > 5 || x.Name == "A") && x.Id > 5;
ConditionBuilderVisitor vistor = new ConditionBuilderVisitor();
vistor.Visit(lambda);
Console.WriteLine(vistor.Condition());
}

7、连接表达式目录树

表达式目录树除了可以修改外,我们还可以通过对其进行表达式目录树的拼接,将两个及其以上的表达式目录树进行拼接在一起。先编写一个新的NewExpressionVisitor,继承自ExpressionVisitor,用于拼接时,调用的。它是一个内部类,放在访问拼接类的内部ExpressionExtend。然后再编写对应的扩展方法:Add、Or、Not


/// <summary>
/// 合并表达式 And Or Not扩展
/// </summary>
public static class ExpressionExtend
{
/// <summary>合并表达式 expLeft and expRight</summary>
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T,bool>> expLeft,Expression<Func<T,bool>> expRight)
{
//用于将参数名进行替换,二者参数不一样
ParameterExpression newParameter = Expression.Parameter(typeof(T), "c");
NewExpressionVisitor visitor = new NewExpressionVisitor(newParameter);
//需要先将参数替换为一致的,可能参数名不一样
var left = visitor.Replace(expLeft.Body);//左侧的表达式
var right = visitor.Replace(expRight.Body);//右侧的表达式
var body = Expression.And(left, right);//合并表达式
return Expression.Lambda<Func<T, bool>>(body, newParameter);
}
/// <summary>合并表达式 expr1 or expr2</summary>
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{

ParameterExpression newParameter = Expression.Parameter(typeof(T), "c");
NewExpressionVisitor visitor = new NewExpressionVisitor(newParameter);
//需要先将参数替换为一致的,可能参数名不一样
var left = visitor.Replace(expr1.Body);
var right = visitor.Replace(expr2.Body);
var body = Expression.Or(left, right);
return Expression.Lambda<Func<T, bool>>(body, newParameter);
}
public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expr)
{
var candidateExpr = expr.Parameters[0];
var body = Expression.Not(expr.Body);
return Expression.Lambda<Func<T, bool>>(body, candidateExpr);
}
/// <summary>参数替换者 </summary>
class NewExpressionVisitor : ExpressionVisitor
{
public ParameterExpression _NewParameter { get; private set; }
public NewExpressionVisitor(ParameterExpression param)
{
 this._NewParameter = param;//用于把参数替换了
}
/// <summary> 替换</summary>
public Expression Replace(Expression exp)
{
 return this.Visit(exp);
}
protected override Expression VisitParameter(ParameterExpression node)
{
 //返回新的参数名
 return this._NewParameter;
}
}
}

下面是测试代码:


Expression<Func<People, bool>> lambda1 = x => x.Age > 5;
Expression<Func<People, bool>> lambda2 = p => p.Id > 5;
Expression<Func<People, bool>> lambda3 = lambda1.And(lambda2);
Expression<Func<People, bool>> lambda4 = lambda1.Or(lambda2);
Expression<Func<People, bool>> lambda5 = lambda1.Not();

List<People> people = new List<People>()
{
new People(){Id=4,Name="123",Age=4},
new People(){Id=5,Name="234",Age=5},
new People(){Id=6,Name="345",Age=6},
};

List<People> lst1 = people.Where(lambda3.Compile()).ToList();
List<People> lst2 = people.Where(lambda4.Compile()).ToList();
List<People> lst3 = people.Where(lambda5.Compile()).ToList();

来源:https://www.cnblogs.com/pilgrim/p/13860856.html

标签:c#,表达式,目录树
0
投稿

猜你喜欢

  • java实现学籍管理系统

    2023-04-03 00:32:19
  • C#特性-迭代器(上)及一些研究过程中的副产品

    2023-12-05 18:26:49
  • java利用多线程和Socket实现猜拳游戏

    2022-10-03 08:03:30
  • 在Spring环境中正确关闭线程池的姿势

    2023-11-25 08:07:29
  • java编程多线程并发处理实例解析

    2022-06-02 22:14:59
  • 浅析Android手机卫士保存手机安全号码

    2021-08-17 20:02:52
  • C#开发Windows服务实例之实现禁止QQ运行

    2023-04-13 03:29:29
  • 浅析c# 线程同步

    2022-09-19 18:43:03
  • c#使用filesystemwatcher实时监控文件目录的添加和删除

    2021-12-04 18:01:57
  • Android Spinner 下拉菜单的使用

    2023-10-25 10:59:53
  • Mybatis 缓存原理及失效情况解析

    2022-12-04 07:28:43
  • SpringSecurity Jwt Token 自动刷新的实现

    2022-04-28 18:49:45
  • Android线程间通信Handler源码详解

    2021-12-31 07:04:10
  • Android实现淘宝客户端倒计时界面

    2023-09-18 21:25:09
  • java fastdfs客户端使用实例代码

    2022-11-19 05:35:59
  • 解析JavaSe的内部类

    2022-08-14 18:19:33
  • SpringBoot深入浅出分析初始化器

    2022-07-06 09:05:59
  • Mybatis环境搭建及文件配置过程解析

    2021-07-04 22:37:03
  • java文件输出流写文件的几种方法

    2023-11-08 16:17:30
  • Springcloud-nacos实现配置和注册中心的方法

    2023-06-15 13:46:42
  • asp之家 软件编程 m.aspxhome.com