在我的 C# 代码中,我有 2 个 WHERE 查询,我可以在 IQueryable 上调用这两个查询,并将整个查询编译成 SQL,这两个查询都有很多共同的逻辑。
我相信这不是这个类似问题的重复: Using Function in Select Clause of Entity Framework Query因为在我的场景中,有问题的函数可以转换为 SQL - EF 只是没有意识到它可以这样做。
查询大概是:
public static IQueryable<Template> WhereIsOwnedByUser(this IQueryable<Template> set, User user)
{
return set.Where(temp =>
temp.Requests
.Where(req => req.WasSent)
.OrderByDescending(req => req.DueDate)
.Take(2)
.SelectMany(req => req.RequestRecipients.Select(reqRecip => reqRecip.Recipient.Id))
.Contains(user.Id));
}
和
public static IQueryable<Template> WhereIsOwnedByUser(this IQueryable<DataReturn> set, User user)
{
return set.Where(ret=>
ret.Entity.Id == user.Entity.Id
&&
ret.Request.Template.Requests
.Where(req => req.WasSent)
.OrderByDescending(req => req.DueDate)
.Take(2)
.SelectMany(req => req.RequestRecipients.Select(reqRecip => reqRecip.Recipient.Id))
.Contains(user.Id));
}
因此,“拥有模板”的基本 BusinessLogic 规则是“如果公司匹配并拥有模板,则拥有 DataReturn”的推论
如您所见,仅考虑 C#,这些可以很容易地重构为:
private static bool UserOwnsTemplate(User user, Template temp)
{
return temp.Requests
.Where(req => req.WasSent)
.OrderByDescending(req => req.DueDate)
.Take(2)
.SelectMany(req => req.RequestRecipients.Select(reqRecip => reqRecip.Recipient.Id))
.Contains(user.Id);
}
public static IQueryable<Template> WhereIsOwnedByUser(this IQueryable<Template> set, User user)
{
return set.Where(temp => UserOwnsTemplate(user, temp));
}
public static IQueryable<DataReturn> WhereIsOwnedByUser(this IQueryable<DataReturn> set, User user)
{
return set.Where(
ret =>
ret.Entity.Id == user.Entity.Id
&&
UserOwnsTemplate(user, ret.Request.Template)
);
}
从而减少重复(耶!)
但是然后 EF 会提示它不知道如何处理 UserOwnsTemplate
,尽管它可以很好地处理 SQL 中的逻辑。
AFAICT 没有很好的方法来解决这个问题。我认为我的选择是:
- 转
UserOwnsTemplate
到一个 UDF 中,一个在数据库中定义的 SQL 函数。- 但我不能从 C# lamda 创建 UDF,我必须定义 SQL,这会更麻烦。
- 分配
Expression<Func<Template,bool>>
那UserOwnsTemplate
定义为变量,然后构建相关Expression<Func<DataReturn ,bool>>
使用Expression.AndAlso
手动获取 DataReturn 版本将两个“子句”粘合在一起。- 元编程。呃。我以前在另一个项目中做过这件事,这样做很卑鄙,维护起来也是一场噩梦。
- 忍受重复。
- 除非 SO 另有建议,否则很可能会发生什么。 ;)
任何人都可以看到任何其他可用的选项吗?
我可以做些什么来强制 EF 将函数解析为 SQL 吗? (我想到了“inling”这个词,但我不是 100% 知道我的意思是什么?)
谁能找到一种方法将 ret.Request.Template 转换成 IQueryable,这样我就可以在其上调用其他 WhereIsOwnedBy 扩展方法?
还有其他建议吗?
最佳答案
您可以保留您的语法并使其正常工作,但您需要在外部 IQueryable<> 上调用一个额外的方法。
诀窍是用一个副本手动替换 IQueryable<>.Expression,在该副本中您用相应的 Expression> 替换函数调用。
所以我的想法是做这样的事情:
public static class MyLinqExtensions
{
public static IQueryable<T> InlineFunctions<T>(this IQueryable<T> queryable)
{
var expression = TransformExpression(queryable.Expression);
return (IQueryable<T>)queryable.Provider.CreateQuery(expression);
}
private static Expression TransformExpression(System.Linq.Expressions.Expression expression)
{
var visitor = new InlineFunctionsExpressionVisitor();
return visitor.Visit(expression);
}
private class InlineFunctionsExpressionVisitor : System.Linq.Expressions.ExpressionVisitor
{
protected override System.Linq.Expressions.Expression VisitMethodCall(System.Linq.Expressions.MethodCallExpression methodCallExpression)
{
if (methodCallExpression.Method.IsStatic
&& methodCallExpression.Method.DeclaringType == typeof(MyDeclaringType)
&& methodCallExpression.Method.Name == "WhereIsOwnedByUser")
{
var setArgumentExpression = methodCallExpression.Arguments[0];
var userArgumentExpression = methodCallExpression.Arguments[1];
var methodInfo = ... // Get typeof(IQueryable<Template>).MethodInfo
var whereConditionExpression = ...// Build where condition and use userArgumentExpression
return Expression.MethodCallExpression(methodInfo, setArgumentExpression, whereConditionExpression);
}
return base.VisitMethodCall(methodCallExpression);
// Some ideas to make this more flexible:
// 1. Use an attribute to mark the functions that can be inlined [InlinableAttribute]
// 2. Define an Expression<Func<>> first to be able to get the Expression and substritute the function call with it:
// Expression<Func<IQueryable<Template>, User, IQueryable<Template>>> _whereIsOwnedByUser = (set, user) =>
// {
// return set.Where(temp => UserOwnsTemplate(user, temp));
// };
//
// public static IQueryable<Template> WhereIsOwnedByUser(this IQueryable<Template> set, User user)
// {
// // You should cache the compiled expression
// return _whereIsOwnedByUser.Compile().Invoke(set, user);
// }
//
}
}
}
然后你可以这样做:
public static IQueryable<DataReturn> WhereIsOwnedByUser(this IQueryable<DataReturn> set, User user)
{
return set.Where(
ret =>
ret.Entity.Id == user.Entity.Id
&&
UserOwnsTemplate(user, ret.Request.Template)
)
.InlineFunctions();
}
关于c# - 在 Entity Framework 查询中使用 C# 函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33124209/