在Entity Framework查询中使用C#函数

9
在我的C#代码中,我有两个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));
}

AND

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));
}

因此,"拥有模板"的基本业务逻辑规则和针对公司匹配且拥有模板的"拥有DataReturn"的推论可以很容易地在C#中进行重构:

如您所见,仅考虑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中完美地处理逻辑。
据我所知,没有什么好的解决方法。我认为我的选择有:
- 将UserOwnsTemplate转换为UDF,即在数据库中定义的SQL函数。 - 但是我不能从C# lambda创建UDF,我必须定义SQL,这将更加麻烦。 - 将Expression>由UserOwnsTemplate定义的分配为变量,然后手动构建与DataReturn版本相关的Expression>,使用Expression.AndAlso将两个“子句”粘合在一起。 - 元编程。呕吐。我以前在另一个项目中做过这个,那是恶心的,而且难以维护。 - 接受重复。 - 除非SO能提供其他建议,否则可能会发生这种情况。;) 你们有其他可用的选项吗?
我能做些什么来强制EF解析函数成SQL?(我想到了“内联”这个词,但我不确定我对它的理解是否正确?)
有人能想到一种将ret.Request.Template转换为IQueryable的方法,以便我只需在其上调用另一个WhereIsOwnedBy扩展方法吗?
还有任何其他建议吗?

1
关于您提到的第二点,我认为AndAlso位操作符不需要任何表达式树操作。您可以始终执行set.Where(expression).Where(anotherExpression)。困难在于您的表达式取决于用户,并且您想将其应用于与IQueryable(temp vs. ret.Request.Template)的根路径不同的路径。我无法看出如何解决这些问题而不构造自己的表达式。可能还有其他的解决办法,但我预计这将涉及到相当大量对你所做查询的更改。 - hgcummings
重复的问题:https://dev59.com/flrUa4cB1Zd3GeqPiUcB? - Dan
2个回答

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); 
            // }
            //
        }
    }
}

And then you can do this:

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目前仅支持设计时重写:https://dev59.com/Imsz5IYBdhLWcg3wiIWe - Guillaume86

0
问题在于你的方法成为了表达式树的一部分,EF 无法对其进行评估。原则上,在触发查询之前,可以评估表达式树的部分。看看 Re-Linq:https://relinq.codeplex.com/ 它有一个名为 PartialEvaluatingExpressionTreeVisitor 的类,可以评估所有部分表达式树,即它会找到你的方法、评估它并注入实际的表达式树。这将付出一定的性能成本,但可能不会很大,你需要权衡干净的设计和性能。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接