NetCore调整表达式目录树实现并发布到NuGet

Posted wosperry

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NetCore调整表达式目录树实现并发布到NuGet相关的知识,希望对你有一定的参考价值。

调整表达式目录树实现并发布到NuGet

上一篇:【NetCore】使用表达式目录树实现动态组装Where的Linq表达式


代码仓库:https://gitee.com/wosperry/perry-expression-practice.git

如果想调试的话,可以安装git后,在控制台运行 git clone https://gitee.com/wosperry/perry-expression-practice.git 获取到代码,直接运行控制台程序即可(类库项目即是NuGet的那个代码)。


​ 上一篇使用表达式目录树实现根据参数类的配置,动态构建Lambda表达式。后面在老张的哲学弄的那个技术群里,群友给建议,让我把枚举值去掉,换成多个Attribute,仔细一想感觉是很有道理。我自己用的时候我也不希望写个特性还非要带一个要指定变量名的又臭又长的参数。

​ 于是就着手去改。花了两个小时,把这些改完了并第一次尝试发布到NuGet,然后把控制台程序修改为安装NuGet包,一次成功,蛮开心的。同时为了配置NuGet里的几个参数,顺便把Gitee项目设置为了Apache2.0开源了。


已经发布到NuGet,安装命令


# 程序包管理控制台
Install-Package Wosperry.ExpressionExtensions -Version 1.0.4

# .Net CLI
dotnet add package Wosperry.ExpressionExtensions --version 1.0.4

# PackageReference XML
<PackageReference Include="Wosperry.ExpressionExtensions" Version="1.0.4" />


使用参考

  • 实体类和参数类:

using Wosperry.ExpressionExtensions.Attributes;

namespace ConsoleApp
{
    public class Student
    {
        public string Name { get; set; }

        public string Code { get; set; }

        public int? Age { get; set; }
    }

    public class StudentListQueryParameter
    {
        [WhereLike]
        public string Name { get; set; }

        [WhereLike("Code")]
        public string Number { get; set; }

        [WhereEqual]
        public int? Age { get; set; }
    } 
}

  • 使用
// 需要引用: using Wosperry.ExpressionExtensions.Extends;

#region 假数据 
List<Student> students = new List<Student> {
    new Student{ Code="A001",Name="张三",Age=25},
    new Student{ Code="A002",Name="李四",Age=18},
    new Student{ Code="A003",Name="王五",Age=10},
    new Student{ Code="B001",Name="张四",Age=18},
    new Student{ Code="B002",Name="李五",Age=12},
    new Student{ Code="B003",Name="王六",Age=45},
    new Student{ Code="C001",Name="张五",Age=22},
    new Student{ Code="C001",Name="李六",Age=18},
    new Student{ Code="C001",Name="王七",Age=20},
};
#endregion

// 使用
var parameter = new StudentListQueryParameter()
{
    //Age = 18,
    Number = "B00",
    Name = "张"
};

// 1.0.3和1.0.4分别增加了一个拓展方法
// 现在不需要手动转IQueryable再调用Where,直接使用集合调用Where方法即可
var data = students.Where(parameter);



// 其他
// var query = students.AsQueryable();
// var data = query.Where(query.BuildLambda(input)).ToList();

// 如果是ABP的IRepository, 默认实现了IQueryable,所以调用如:
// var query = _repository.Where(_repository.BuildLambda(input));
// var data = await AsyncExecuter.ToListAsync(query);

展示一下调整后的代码,同样是希望能够帮助到未来的自己,还有暂时没有接触过这个东西的同行们。

事实上代码没有那么多,只是注释太密集了,没办法,我怕过两天我自己看不懂,多写了点注释,如果不想看那么多注释,可以拷贝出去删掉看。

public static class BinaryExpressionExtensions
{
    /// <summary>
    /// 连接一个Like(字符串的Contains)
    /// </summary>
    /// <param name="resultExpression">表达式</param>
    /// <param name="field">实体属性/字段</param>
    /// <param name="valueToCompare">对比的值</param>
    /// <param name="stringComparison">字符串对比规则</param>
    /// <returns> 返回示例: xxx && t.Contains(valueToCompare)</returns>
    public static BinaryExpression AndLikeLambda(this BinaryExpression resultExpression, MemberExpression field, ConstantExpression valueToCompare, StringComparison stringComparison = default)
    {
        // Contains 方法
        var containMethod = typeof(string).GetMethod(
            nameof(string.Contains),
            new Type[] { typeof(string), typeof(StringComparison) }
        );
        // 表达式:Contains 方法
        var containMethodExpression = Expression.Call(
            field,
            containMethod,
            valueToCompare,
            Expression.Constant(stringComparison)
        );
        return Expression.AndAlso(resultExpression, containMethodExpression);
    }

    /// <summary>
    /// 连接一个 Equals
    /// </summary>
    /// <param name="resultExpression"></param>
    /// <param name="field"></param>
    /// <param name="valueToCompare"></param>
    /// <returns></returns>
    public static BinaryExpression AndEqualLambda(this BinaryExpression resultExpression, MemberExpression field, ConstantExpression valueToCompare)
    {
        return Expression.AndAlso(
                resultExpression,
                Expression.Equal(
                    field,
                    Expression.Convert(valueToCompare, field.Type)
                )
            );
    }
}
public static class IQuaryableExtension
{
    /// <summary>
    /// 生成查询条件
    /// </summary>
    /// <typeparam name="TQueryParams">查询参数对象</typeparam>
    /// <param name="queryable">原有IQueryable</param>
    /// <exception cref="WhereAttribute.Field">
    /// 当 WhereAttribute.Field 指定的字段名在TEntity类型中不存在的时候,抛出异常
    /// </exception>
    /// <returns>lambda 表达式</returns>
    public static Expression<Func<TEntity, bool>> BuildLambda<TEntity, TQueryParams>(this IQueryable<TEntity> query, TQueryParams input, Action<BuildWhereOptions> options = null) where TEntity : class
    {
        // 配置项
        BuildWhereOptions buildWhereOptions = new BuildWhereOptions();
        if (!(options is null))
        {
            options(buildWhereOptions);
        }

        var t = Expression.Parameter(typeof(TEntity), "t");
        // 表达式:true
        var trueExpression = Expression.Constant(true);
        // 表达式:true && true
        var result = Expression.AndAlso(trueExpression, trueExpression);

        // 遍历入参所有属性 
        foreach (var prop in typeof(TQueryParams).GetProperties())
        {
            // 根据特性分别处理
            var attrs = prop.GetCustomAttributes();

            // 如果没有任何特性,则使用默认 DefaultAttribute 

            foreach (WhereAttribute attribute in attrs.Where(w => w is WhereAttribute))
            {
                // 表达式:实体属性 t
                //      当attr未指定 Field 值的时候,默认为与入参同名
                //      党attr指定了 Field 值的时候,使用 Field
                if (string.IsNullOrWhiteSpace(attribute.Field))
                {
                    attribute.Field = prop.Name;
                }

                // 对比的参数为空时,不做处理
                var valueToCompare = prop.GetValue(input);
                if (valueToCompare is null)
                    continue;

                // string 时,单独判断一次是否是空值
                if (prop.PropertyType == typeof(string) && string.IsNullOrWhiteSpace(valueToCompare as string))
                    continue;

                // 表达式实体属性:t.Name
                var fieldExpression = Expression.Property(t, attribute.Field);

                // 需要对比的值:value
                var valueExpression = Expression.Constant(valueToCompare, prop.PropertyType);

                #region 构建表达式

                // 相等
                // t.Name.Equals(value)
                if (attribute is WhereEqualAttribute)
                {
                    result = result.AndEqualLambda(fieldExpression, valueExpression);
                }

                // Like(字符串的Contains)
                // t.Name.Contains(value, builderWhereOptions.StringComparison)
                if (attribute is WhereLikeAttribute)
                {
                    result = result.AndLikeLambda(fieldExpression, valueExpression, buildWhereOptions.StringComparison);
                }

                // TODO:增加其他的对比类型(Larger、Less、In)

                #endregion
            }
        }
        // 如:t=>t.Age.Equals(input.Age) && t.Code.Contains(input.Number) && t.Name.Contains(input.Name)
        // 注意:input.Number是因为其有特性 [WhereLike("Code")]
        return Expression.Lambda<Func<TEntity, bool>>(result, t);
    }
}

使用表达式目录树实现SqlDataReader到实体的映射

SqlDataReader映射实体,是ORM的基础功能,常见的实现方式有反射、表达式目录树和emit,这里要说的就是用表达式目录树生成实体的方法。

先分析下思路:

假设有个数据实体类,Student

public class Student
{
    public int Id { get; set; }

    public string Name { get; set; }

    public int ClazzId { get; set; }
}

获取到SqlDataReader,手撸代码的话,我们可能是这样做的

public Student ConvertToStudent(SqlDataReader sdr)
{
    var entity = new Student
    {
        Id = sdr.GetInt32(0),
        Name = sdr.GetString(1),
        ClazzId = sdr.GetInt32(2)
    };

    return entity;
}

无疑,这种效率是最高的,原因是:一、直接通过索引获取Reader内的数据;二、直接为属性赋值,而无需通过PropertyInfo.SetValue,既避开了反射,又无需对数据类型进行转换,减少了装箱拆箱的操作。

但是,我们需要的是一个通用方法,上面的写法只能针对具体类型,所以我们真正需要的是这样一个类似的泛型版本

public T ConvertToEntity<T>(SqlDataReader reader) where T : new()
{
    var t = new T();

    t.a = reader.GetInt32(0);
    t.b = reader.GetString(1);

    return t;
}

问题来了,我们并不知道实体的具体定义,所以上面的代码根本无法实现。所以现在需要解决两个问题:

1、要获取到T的所有属性,这里假设实体是个POCO模型

2、为T专门构建一个方法,实现类似ConvertToStudent()那样的功能

第一个问题很简单,反射,要动态获取实体的属性,这一步是不可避免的,通用做法就是先做一次反射,然后把实体的PropertyInfo缓存起来,这样只要反射一次,这点性能损耗还是可以承受的

第二个问题比较麻烦,动态生成方法,在我的知识体系内是没有办法的,但恰巧知道一个动态生成委托的方法,那就是表达式目录树,我们知道,委托delegate 就是对方法的封装,动态生成委托,正好可以解决眼下的问题

现在将ConvertToStudent改造成委托,这段代码就是我们构造表达式目录树的模板

private Func<SqlDataReader, Student> ConvertToStudentFunc = (reader) =>
{
    var entity = new Student();
    entity.Id = reader.GetInt32(0);
    entity.Name = reader.GetString(1);
    entity.ClazzId = reader.GetInt32(2);
    return entity;
};

因为没有找到系统的表达式目录树的资料,所以对语法不甚了解,下面的代码是结合网上的资料一点一点摸索出来的,具体不解释了,怕说错了误人子弟,直接看代码吧:

private static Func<SqlDataReader, T> Converter<T>(IDataReader reader)
{
    var sdrParameter = Expression.Parameter(typeof(SqlDataReader), "sdr");

    var memberBindings = new List<MemberBinding>();

    var properties = typeof(T).GetProperties().Where(p => p.CanWrite);

    for (var i = 0; i < reader.FieldCount; i++)
    {
        var fieldName = reader.GetName(i);
        var property =
            properties.SingleOrDefault(p => p.Name == fieldName && p.PropertyType == reader.GetFieldType(i));

        if (property == null) continue;

        var methodName = "GetValue";
        if (property.PropertyType == typeof(string))
        {
            methodName = "GetString";
        }
        else if (property.PropertyType == typeof(int))
        {
            methodName = "GetInt32";
        }
        else if (property.PropertyType == typeof(DateTime))
        {
            methodName = "GetDateTime";
        }
        else if (property.PropertyType == typeof(decimal))
        {
            methodName = "GetDecimal";
        }
        else if (property.PropertyType == typeof(Guid))
        {
            methodName = "GetGuid";
        }
        else if (property.PropertyType == typeof(bool))
        {
            methodName = "GetBoolean";
        }
        else
        {
            continue;
        }

        var methodCall = Expression.Call(sdrParameter,
            typeof(SqlDataReader).GetMethod(methodName) ?? throw new InvalidOperationException(),
            Expression.Constant(i));

        memberBindings.Add(Expression.Bind(property, methodCall));
    }

    var initExpression = Expression.MemberInit(Expression.New(typeof(T)), memberBindings);
    return Expression.Lambda<Func<SqlDataReader, T>>(initExpression, sdrParameter).Compile();
}

代码并不严谨,很多东西没做,只是个框框,理解原理用的,下面是使用方法:

public static List<T> Fetch<T>(int count)
{
    var list = new List<T>();
    Func<SqlDataReader, T> func = null;
    var topStr = count <= 0 ? "" : $"TOP {count}";
    using (var conn = new SqlConnection(ConfigurationManager.AppSettings["DbConnection"]))
    {
        using (var command = new SqlCommand($"SELECT {topStr} * FROM {typeof(T).Name}", conn))
        {
            conn.Open();
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    if (func == null)
                    {
                        func = ExpressionShow.ConvertEntity<T>(reader);
                        list.Add(func(reader));
                    }
                    else
                    {
                        list.Add(func(reader));
                    }
                }
                reader.Close();
            }
        }
    }

    return list;
}

到这里,基本功能就完成了。

 

以上是关于NetCore调整表达式目录树实现并发布到NuGet的主要内容,如果未能解决你的问题,请参考以下文章

(VIP-朝夕教育)2021-05-27 .NET高级班 15-解析表达式目录树实现替换

通过表达式目录树拼装lambda表达式,来实现两个类型的复制

表达式目录树

netcore开发windows普通服务(非Web)并一键发布到服务器

AVLTree的C++的实现

使用红黑树封装map和set