如何在运行时创建表达式以在 GroupBy() 中与实体框架一起使用
本文关键字:实体 框架 一起 运行时 创建 表达式 GroupBy | 更新日期: 2023-09-27 18:33:58
我正在构建一个新的扩展方法,该方法将能够动态地对来自实体框架的查询结果进行分组。
我已经能够使用 LinqKit 构建动态的"where"表达式,但这似乎是一种不同的动物。
新扩展方法的预期用法:
var results = entities.GroupBy("someFieldName").ToList();
扩展方法定义:
public static IQueryable<IGrouping<object, TEntity>> GroupBy<TEntity>(
this IQueryable<TEntity> source,
string fieldName)
where TEntity : class, IDataEntity
{
if (string.IsNullOrEmpty(fieldName))
{
return new List<IGrouping<object, TEntity>>().AsQueryable();
}
var parameter = Expression.Parameter(typeof(TEntity), "x");
var fieldXExpression = Expression.Property(parameter, fieldName);
var lambda = Expression.Lambda<Func<TEntity, object>>(
Expression.Convert(fieldXExpression, typeof(object)), // throws error when using EF
parameter);
return source.AsExpandable().GroupBy(lambda);
}
我需要使用 Expression.Convert(...)
,因为当我使用 linqTOobjects 进行测试时,当列是int32
时代码失败。所以我需要手动将列转换为对象,效果很好。
现在我正在使用 EF 实体对其进行测试,我猜 EF 正在尝试将转换转换为等效的 SQL,当然我知道这并不存在。
错误:
System.NotSupportedException: Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.
有没有办法在运行时生成适合 GroupBy 且也与 EF 兼容的表达式?
感谢您的任何指导。
遗憾的是,不能按在实体框架中键入为object
的值进行分组。我认为有两种选择:您可以从该方法返回一个IQueryable
(非泛型(对象,如果您正在对结果执行数据绑定之类的操作,这将起作用,但对其他很多事情都不好。这允许您忽略分组所依据的属性的类型。
public static IQueryable GroupBy<TEntity>(
IQueryable<TEntity> source,
string fieldName)
where TEntity : class, IDataEntity
{
if (string.IsNullOrEmpty(fieldName))
{
return new List<IGrouping<object, TEntity>>().AsQueryable();
}
var parameter = Expression.Parameter(typeof(TEntity), "x");
var propertyAccess = Expression.Property(parameter, fieldName);
var lambda = Expression.Lambda(propertyAccess, parameter);
var groupExpression = Expression.Call(
typeof(Queryable).GetMethods()
.First (x => x.Name == "GroupBy")
.MakeGenericMethod(new[]{ typeof(TEntity), propertyAccess.Type }),
source.Expression,
lambda);
var result = source.Provider.CreateQuery(groupExpression);
return result;
}
从此方法返回的IQueryable
将是正确的类型(如果fieldName
引用int
属性,则可以将结果强制转换为IQueryable<IGrouping<int,TEntity>>
(,但是很难编写可以利用这一事实的代码,并且如果fieldName
是int
,您将无法将其转换为IQueryable<IGrouping<object,TEntity>>
, 因为协方差不适用于值类型。
仅当您从调用位置知道属性的类型时,另一个选项才可用,以便您可以在方法参数中提供分组键的类型(这是能够返回正确类型的通用 IQueryable 的唯一方法(:
public static IQueryable<IGrouping<TProperty,TEntity>> GroupBy<TEntity, TProperty>(
IQueryable<TEntity> source,
string fieldName)
where TEntity : class, IDataEntity
{
if (string.IsNullOrEmpty(fieldName))
{
return new List<IGrouping<TProperty, TEntity>>().AsQueryable();
}
var parameter = Expression.Parameter(typeof(TEntity), "x");
var propertyAccess = Expression.Property(parameter, fieldName);
var lambda = Expression.Lambda<Func<TEntity, TProperty>>(propertyAccess, parameter);
var result = source.GroupBy (lambda);
return result;
}
我能够通过使用动态 Linq 完成我需要的事情。我创建了一个名为ListGroupKeys的扩展方法:
public static List<object> ListGroupKeys<TEntity>(this IQueryable<TEntity> source, string fieldName)
where TEntity : class, IDataEntity
{
var results = source.GroupBy(fieldName, "it").Select("new (KEY as Group)");
var list = new List<object>();
foreach (var result in results)
{
var type = result.GetType();
var prop = type.GetProperty("Group");
list.Add(prop.GetValue(result));
}
return list;
}
这允许我获取组键值以用于后续查询。