【问题标题】:Using C# function in Entity Framework Query在实体框架查询中使用 C# 函数
【发布时间】:2015-10-14 11:33:05
【问题描述】:

在我的 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,这样会比较麻烦。
  • UserOwnsTemplate 定义的Expression&lt;Func&lt;Template,bool&gt;&gt; 分配为变量,然后使用Expression.AndAlso 为DataReturn 版本手动构建相关的Expression&lt;Func&lt;DataReturn ,bool&gt;&gt; 将两个“子句”粘合在一起。
    • 元编程。呃。我以前在另一个项目中做过这件事,做起来很卑鄙,维护起来简直是一场噩梦。
  • 接受副本。
    • 除非 SO 另有建议,否则可能会发生什么情况。 ;)

任何人都可以看到任何其他可用的选项吗?

我可以做些什么来强制 EF 将函数解析为 SQL 吗? (我想到了“inling”这个词,但我不是 100% 知道我认为我的意思是什么?)

谁能看到将 ret.Request.Template 转换为 IQueryable 的方法,以便我可以在其上调用其他 WhereIsOwnedBy 扩展方法?

还有其他建议吗?

【问题讨论】:

  • 关于您的第二个要点,我认为 AndAlso 位不需要任何表达式树争论。您可以随时使用set.Where(expression).Where(anotherExpression)。困难在于您的表达式依赖于用户,并且您希望将其应用到与 IQueryable 的根不同的路径(temp 与 ret.Request.Template)。如果不构建自己的表达式,我看不出如何解决这些问题。可能有另一种解决方法,但我希望它涉及到实质性地改变你正在做的查询。

标签: c# sql-server entity-framework function


【解决方案1】:

您可以保留您的语法并使其正常工作,但您需要在外部 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();
}

【讨论】:

  • 一些要点:使用 AOP 框架(重写一些代码的预构建步骤),运行时成本将为零。您只需用属性标记您的函数,AOP 将用函数的内容替换函数调用。太糟糕了 Roslyn 目前只支持设计时重写:stackoverflow.com/questions/7833954/…
【解决方案2】:

问题是您的方法成为表达式树的一部分并且 EF 无法评估它。原则上,可以在触发查询之前评估表达式树的某些部分。看看 Re-Linq:https://relinq.codeplex.com/ 它有一个类 PartialEvaluatingExpressionTreeVisitor 可以评估所有部分表达式树,即它会找到你的方法,评估它,并注入实际的表达式树。这会带来一定的性能成本,但它可能并不重要,您必须衡量干净的设计与性能。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-02
    • 1970-01-01
    相关资源
    最近更新 更多