创建Expression表达式树动态查询

EFCore 使用反射 根据实体的属性动态生成Expression进行查询

EFCore 的查询语法是:

Expression<Func<T,bool>> condition = x=>x.ClubId==1 ;
dbContext.Set<T>().Where(condition).ToList();

而我想通过传入一个T实体,根据T被赋值的属性来生成condition,用了反射和表达式树。不知道有没有大神告诉下其他的办法。

service

public List<PlayersEntity> GetPlayers(PlayersEntity queryModel)
 {
       //获取类的属性
       PropertyInfo[] properties = queryModel.Players.GetType().GetProperties();
       BinaryExpression condtion = null;
       //Expression<Func<PlayersEntity, bool>> expression;
       var param = Expression.Parameter(typeof(PlayersEntity), "x"); //x=>
       //遍历属性
       foreach (PropertyInfo property in properties)
       {
           var value = property.GetValue(queryModel);
           //空值判断
           if (value == null)
               continue;
           if (int.TryParse(value.ToString(), out int temp) && value.ToString() == "0")
               continue;
           if (string.IsNullOrEmpty(property.GetValue(queryModel).ToString()))
               continue;

           //表达式树的构建
           MemberExpression left = Expression.Property(param , property);  //x.name
           ConstantExpression right = Expression.Constant(value, property.PropertyType); // value
           BinaryExpression be = Expression.Equal(left, right); //x=>x.name == value
           if (condtion == null)
           {
               condtion = be;
           }
           else
           {
               //拼接表达式树
               condtion = Expression.And(condtion, be);
           }
       }
       //生成表达式树
       Expression<Func<PlayersEntity, bool>> expression = Expression.Lambda<Func<PlayersEntity, bool>>(condtion, param ); 
       //后面的两个参数是用来分页的
       return (List<PlayersEntity>)_database.FindListByIndex(out int temp2, expression, null, 1, 5);
   }

数据库层

public IEnumerable<T> FindListByIndex<T>(out int total,Expression<Func<T,bool>> filter =null,Func<IQueryable<T>,IOrderedQueryable<T>> orderBy =null,int index = 1,int size =20) where T : class
  {
      int skipCount = (index - 1) * size;
      var _reset = Find(filter, orderBy);
      total = _reset.Count();
      _reset = skipCount > 0 ? _reset.Skip(skipCount).Take(size) : _reset.Take(size);
      return _reset.ToList();
  }
     
private IQueryable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null) where TEntity : class
   {
        IQueryable<TEntity> query = dbContext.Set<TEntity>();
        if (filter != null)
        {
            query = query.Where(filter);
        }
        if (orderBy != null)
        {
            return orderBy(query).AsQueryable();
        }
        else
        {
            return query.AsQueryable();
        }
    }

若想使用函数,例如stringContains
eg:

if(typeof(string) == property.PropertyType)
 {
     MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
     var containsMethodExp = Expression.Call(left, method, right);
     //x=>x.Name.Contains("value")
     if (condtion == null)
         condtion = containsMethodExp;
     else
         condtion = Expression.And(condtion, containsMethodExp);
 }

EF Core根据多个条件更新_通过Expression表达式树,为EF Core找回AddOrUpdate方法

在做.NET Core开发工作的开发者们应该都知道,在EF Core之后,微软大大再也没有提供AddOrUpdate方法,但实际开发当中经常会有AddOrUpdate这样的需求,虽然能够自己根据业务进行定制化的实现,但有个通用的AddOrUpdate方法就会很实用了,而且,博主在曾经.NET Framework时代,也非常喜欢EF6的AddOrUpdate方法,经过了这么多年的不习惯,也在网络上寻找过各路方法,也不太行,虽然也有实现的,但不完美,那么今天,就再前人的坑的基础之上,给EF Core找回AddOrUpdate方法吧!

基本思路

DbSet<T>类型添加一个扩展方法,然后我需要根据哪个属性进行数据的存在性判断,比如Id,或是手机号、身份证号等唯一键进行查存,由于查存的字段的不确定性,所以需要动态构建whereExpression,然后便可以判断传入的实体是需要新增还是更新操作。

逻辑实现

首先创建一个AddOrUpdate方法,为DbSet扩展:

public static void AddOrUpdate<T, TKey>(this DbSet<T> dbSet, Expression<Func<T, TKey>> keySelector, T entity)

其中参数keySelector是一个表达式树类型,决定传入的实体T entity根据哪个字段进行存在性判断。

如何根据keySelector表达式树来知道字段的名字是什么呢?通过表达式树编译,然后反射就知道了,通过调用keySelectorCompile方法,编译成一个Func类型的匿名函数,将entity传入func匿名函数进行调用,便可以得到entity的判重字段TKey的值,比如keySelector是一个根据字符串类型的Name查重的表达式,那么keySelector编译之后传入entity调用拿到的结果就是Name的值,代码如下:

Expression<Func<T, string>> keySelector=e=>e.Name;
var keyObject = keySelector.Compile()(entity); // 此时keyObject是一个string类型的值,该值就是entity的Name的值

但是在构造where表达式树之前,我们还需要做一件事情,就是封装Expression表达式树的参数访问,以便后续重用,在构造where表达式树,会用到表达式树的两种操作,分别是成员访问和创建新对象这两种操作,来生成我们需要的条件表达式树主体部分。比如讲上面的e=>e.Name表达式转换成e=>e.Name=="白火石"这样的形式。代码如下:

private static Expression ReplaceParameter(Expression oldExpression, ParameterExpression newParameter)
{
    return oldExpression.NodeType switch
    {
        ExpressionType.MemberAccess => Expression.MakeMemberAccess(newParameter, ((MemberExpression)oldExpression).Member),
        ExpressionType.New => Expression.New(((NewExpression)oldExpression).Constructor, ((NewExpression)oldExpression).Arguments.Select(a => ReplaceParameter(a, newParameter)).ToArray()),
        _ => throw new NotSupportedException("不支持的表达式类型:" + oldExpression.NodeType)
    };
}

然后便可以构造where表达式树了;

e=>e.Name=="白火石"为例,我们来将表达式拆分,其中e代表的是参数,Name则是参数e的成员,==是判等操作,"白火石"是常量(作为表达式本身来说是个常量,对于外部传入它仍然是变量),对应到表达式树的代码上,分别就是:

参数:Expression.Parameter

成员:Expression.MakeMemberAccess

判等:Expression.Equal

常量值:Expression.Constant

生成表达式:Expression.Lambda<Func<T, bool>>

那如何根据传入的e=>e.Name和实体{Name:"白火石"}构造出e=>e.Name=="白火石"这个where表达式呢?

  1. 拿到判存的值,即表达式的常量Name
var keyObject = keySelector.Compile()(entity);
  1. 构造参数e
var parameter = Expression.Parameter(typeof(T), "p");
  1. 通过上面封装的方法构造成员e.Name
ReplaceParameter(keySelector.Body, parameter);
  1. 构造常量Name
Expression.Constant(keyObject)
  1. 组装Lambda表达式:
var lambda =Expression.Lambda<Func<T, bool>>(Expression.Equal(ReplaceParameter(keySelector.Body, parameter), Expression.Constant(keyObject)), parameter);

where的条件表达式便组装完毕,可以传入到DBSet去查询了:

var item = dbSet.FirstOrDefault(lambda);

现在,就可以根据item是否为null来做是新增还是更新的操作了;

item==null,则DBSet直接Add即可,否则就是更新,但更新的操作,就稍微麻烦一点,由于更新是反射操作,所以需要排除主键字段和判重字段,不过接下来的都是小case了;

获取主键字段:

// 获取主键字段
var dataType = typeof(T);
var keyFields = dataType.GetProperties().Where(p => p.GetCustomAttribute<KeyAttribute>() != null).ToList();
if (!keyFields.Any())
{
    string idName = dataType.Name + "Id";
    keyFields = dataType.GetProperties().Where(p => p.Name.Equals("Id", StringComparison.OrdinalIgnoreCase) || p.Name.Equals(idName, StringComparison.OrdinalIgnoreCase)).ToList();
}

开始更新非主键字段的值:

// 更新所有非主键属性
foreach (var p in typeof(T).GetProperties().Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null))
{
    // 忽略主键
    if (keyFields.Any(x => x.Name == p.Name))
    {
        continue;
    }
 
    var existingValue = p.GetValue(entity);
    if (p.GetValue(item) != existingValue)
    {
        p.SetValue(item, existingValue);
    }
}
 
foreach (var idField in keyFields.Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null))
{
    var existingValue = idField.GetValue(item);
    if (idField.GetValue(entity) != existingValue)
    {
        idField.SetValue(entity, existingValue);
    }
}

至此,AddOrUpdate完美实现,完整代码如下:

/// <summary>
/// 添加或更新
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TKey">按哪个字段更新</typeparam>
/// <param name="dbSet"></param>
/// <param name="keySelector">按哪个字段更新</param>
/// <param name="entity"></param>
public static void AddOrUpdate<T, TKey>(this DbSet<T> dbSet, Expression<Func<T, TKey>> keySelector, T entity) where T : class
{
    if (keySelector == null)
    {
        throw new ArgumentNullException(nameof(keySelector));
    }
 
    if (entity == null)
    {
        throw new ArgumentNullException(nameof(entity));
    }
 
    var keyObject = keySelector.Compile()(entity);
    var parameter = Expression.Parameter(typeof(T), "p");
    var lambda = Expression.Lambda<Func<T, bool>>(Expression.Equal(ReplaceParameter(keySelector.Body, parameter), Expression.Constant(keyObject)), parameter);
    var item = dbSet.FirstOrDefault(lambda);
    if (item == null)
    {
        dbSet.Add(entity);
    }
    else
    {
        // 获取主键字段
        var dataType = typeof(T);
        var keyFields = dataType.GetProperties().Where(p => p.GetCustomAttribute<KeyAttribute>() != null).ToList();
        if (!keyFields.Any())
        {
            string idName = dataType.Name + "Id";
            keyFields = dataType.GetProperties().Where(p => p.Name.Equals("Id", StringComparison.OrdinalIgnoreCase) || p.Name.Equals(idName, StringComparison.OrdinalIgnoreCase)).ToList();
        }
 
        // 更新所有非主键属性
        foreach (var p in typeof(T).GetProperties().Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null))
        {
            // 忽略主键
            if (keyFields.Any(x => x.Name == p.Name))
            {
                continue;
            }
 
            var existingValue = p.GetValue(entity);
            if (p.GetValue(item) != existingValue)
            {
                p.SetValue(item, existingValue);
            }
        }
 
        foreach (var idField in keyFields.Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null))
        {
            var existingValue = idField.GetValue(item);
            if (idField.GetValue(entity) != existingValue)
            {
                idField.SetValue(entity, existingValue);
            }
        }
    }
}
 
private static Expression ReplaceParameter(Expression oldExpression, ParameterExpression newParameter)
{
    return oldExpression.NodeType switch
    {
        ExpressionType.MemberAccess => Expression.MakeMemberAccess(newParameter, ((MemberExpression)oldExpression).Member),
        ExpressionType.New => Expression.New(((NewExpression)oldExpression).Constructor, ((NewExpression)oldExpression).Arguments.Select(a => ReplaceParameter(a, newParameter)).ToArray()),
        _ => throw new NotSupportedException("不支持的表达式类型:" + oldExpression.NodeType)
    };
}

调用示例:

db.SystemSetting.AddOrUpdate(s => s.Name,new SystemSetting()
{
    Value = "2333"
});

该功能已完整封装到Masuit.Tools类库中,方便直接开箱即用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值