有人可以向我解释为什么EF引擎在以下情况下失败了吗?
使用以下表达式可以正常工作:
var data = context.Programs
.Select(d => new MyDataDto
{
ProgramId = d.ProgramId,
ProgramName = d.ProgramName,
ClientId = d.ClientId,
Protocols = d.Protocols.Where(p => p.UserProtocols.Any(u => u.UserId == userId))
.Count(pr => pr.Programs.Any(pg => pg.ProgramId == d.ProgramId))
})
.ToList();
但如果我将一些内容封装到扩展方法中:
public static IQueryable<Protocol> ForUser(this IQueryable<Protocol> protocols, int userId)
{
return protocols.Where(p => p.UserProtocols.Any(u => u.UserId == userId));
}
结果查询:
var data = context.Programs
.Select(d => new MyDataDto
{
ProgramId = d.ProgramId,
ProgramName = d.ProgramName,
ClientId = d.ClientId,
Protocols = d.Protocols.ForUser(userId)
.Count(pr => pr.Programs.Any(pg => pg.ProgramId == d.ProgramId))
})
.ToList();
失败,例外情况:LINQ to Entities无法识别方法&#39; System.Linq.IQueryable1 [DAL.Protocol] ForUser(System.Linq.IQueryable1 [DAL.Protocol],Int32)&#39;方法,并且此方法无法转换为商店表达式。
我希望EF Engine能够构建整个表达式树,链接必要的表达式然后生成SQL。为什么不这样做呢?
答案 0 :(得分:7)
这种情况正在发生,因为ForUser()
的调用是在C#编译器看到传递给Select的lambda时构建的表达式树内部进行的。实体框架试图弄清楚如何将该函数转换为SQL,但由于某些原因它无法调用该函数(例如,此时d.Protocols
不存在)。
适用于这种情况的最简单方法是让你的助手返回一个标准lambda表达式,然后自己将其传递给.Where()
方法:
public static Expression<Func<Protocol, true>> ProtocolIsForUser(int userId)
{
return p => p.UserProtocols.Any(u => u.UserId == userId);
}
...
var protocolCriteria = Helpers.ProtocolIsForUser(userId);
var data = context.Programs
.Select(d => new MyDataDto
{
ProgramId = d.ProgramId,
ProgramName = d.ProgramName,
ClientId = d.ClientId,
Protocols = d.Protocols.Count(protocolCriteria)
})
.ToList();
当您在表达式树之外调用LINQ方法时(就像使用context.Programs.Select(...)
一样),实际调用Queryable.Select()
扩展方法,并且其实现返回表示IQueryable<>
的{{1}}在原始IQueryable<>
上调用扩展方法。这是Select的实现,例如:
public static IQueryable<TResult> Select<TSource,TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector) {
if (source == null)
throw Error.ArgumentNull("source");
if (selector == null)
throw Error.ArgumentNull("selector");
return source.Provider.CreateQuery<TResult>(
Expression.Call(
null,
GetMethodInfo(Queryable.Select, source, selector),
new Expression[] { source.Expression, Expression.Quote(selector) }
));
}
当可查询的提供者必须从IQueryable<>
生成实际数据时,它会分析表达式树并尝试找出如何解释这些方法调用。实体框架具有many LINQ-related functions的内置知识,如.Where()
和.Select()
,因此它知道如何将这些方法调用转换为SQL。但是,它并不知道如何处理您编写的方法。
那为什么会这样呢?
var data = context.Programs.ForUser(userId);
答案是您的ForUser
方法没有像上面的Select
方法那样实现:您没有在查询中添加表达式来表示调用ForUser
。相反,您将返回.Where()
电话的结果。从IQueryable<>
的角度来看,它就像直接调用Where()
一样,并且对ForUser()
的调用从未发生过。
您可以通过捕获Expression
上的IQueryable<>
属性来证明这一点:
Console.WriteLine(data.Expression.ToString());
...会产生这样的东西:
Programs.Where(u => (u.UserId == value(Helpers<>c__DisplayClass1_0).userId))
在该表达式的任何地方都没有调用ForUser()
。
另一方面,如果您在表达式树中包含ForUser()
调用,如下所示:
var data = context.Programs.Select(d => d.Protocols.ForUser(id));
...然后.ForUser()
方法实际上从未被调用过,因此它永远不会返回知道调用IQueryable<>
方法的.Where()
。相反,可查询的表达式树显示.ForUser()
被调用。输出其表达式树看起来像这样:
Programs.Select(d => d.Protocols.ForUser(value(Repository<>c__DisplayClass1_0).userId))
实体框架不知道ForUser()
应该做什么。就其而言,您可以编写ForUser()
来做一些在SQL中无法做到的事情。所以它告诉你,这不是一种受支持的方法。
答案 1 :(得分:0)
正如我在上面的评论中所提到的,我无法说明为什么EF引擎按照它的方式工作。因此,我试图找到一种重新编写查询的方法,以便我能够使用我的扩展方法。
表格是:
Program -> 1..m -> ProgramProtocol -> m..1 -> Protocol
ProgramProtocol只是一个连接表,并未由Entity Framework在模型中映射。 这个想法很简单:选择&#34;从左边&#34;,选择&#34;从右边&#34;然后加入结果集以进行适当的过滤:
var data = context.Programs.ForUser(userId)
.SelectMany(pm => pm.Protocols,
(pm, pt) => new {pm.ProgramId, pm.ProgramName, pm.ClientId, pt.ProtocolId})
.Join(context.Protocols.ForUser(userId), pm => pm.ProtocolId,
pt => pt.ProtocolId, (pm, pt) => pm)
.GroupBy(pm => new {pm.ProgramId, pm.ProgramName, pm.ClientId})
.Select(d => new MyDataDto
{
ProgramName = d.Key.ProgramName,
ProgramId = d.Key.ProgramId,
ClientId = d.Key.ClientId,
Protocols = d.Count()
})
.ToList();