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();
}
}
若想使用函数,例如string
的Contains
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,或是手机号、身份证号等唯一键进行查存,由于查存的字段的不确定性,所以需要动态构建where
的Expression
,然后便可以判断传入的实体是需要新增还是更新操作。
逻辑实现
首先创建一个AddOrUpdate
方法,为DbSet
扩展:
public static void AddOrUpdate<T, TKey>(this DbSet<T> dbSet, Expression<Func<T, TKey>> keySelector, T entity)
其中参数keySelector
是一个表达式树类型,决定传入的实体T entity
根据哪个字段进行存在性判断。
如何根据keySelector
表达式树来知道字段的名字是什么呢?通过表达式树编译,然后反射就知道了,通过调用keySelector
的Compile
方法,编译成一个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
表达式呢?
- 拿到判存的值,即表达式的常量
Name
:
var keyObject = keySelector.Compile()(entity);
- 构造参数
e
:
var parameter = Expression.Parameter(typeof(T), "p");
- 通过上面封装的方法构造成员
e.Name
:
ReplaceParameter(keySelector.Body, parameter);
- 构造常量
Name
:
Expression.Constant(keyObject);
- 组装
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
类库中,方便直接开箱即用。