在实体框架查询中使用c#函数

本文关键字:函数 实体 框架 查询 | 更新日期: 2023-09-27 18:17:34

在我的c#代码中,我有2个WHERE查询,我可以调用一个IQueryable,并将整个事情编译成SQL,两者都有大量的公共逻辑。

我相信这不是一个类似问题的重复:在实体框架查询的选择子句中使用函数,因为在我的场景中,有问题的函数可以转换为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));
}

所以一个基本的商业逻辑规则"拥有一个模板",然后是"拥有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中的逻辑。

没有很好的方法来解决这个问题。我想我的选择是:

  • UserOwnsTemplate转换为UDF,即数据库中定义的SQL函数。
    • 但是我不能从c# lambda创建UDF,我必须定义SQL,这将更麻烦。
  • UserOwnsTemplate定义为变量的Expression<Func<Template,bool>>赋值,然后手工使用Expression.AndAlso将两个"子句"粘合在一起,为DataReturn版本构建相关的Expression<Func<DataReturn ,bool>>
    • 元编程。Ughhh。我以前在另一个项目中这样做过,做起来很讨厌,而且维护起来像噩梦。
  • 使用副本。
      很可能会发生的事情,除非SO能提出其他建议。)

有人看到其他可用的选项吗?

我可以做任何事情来强制EF解析成SQL函数吗?(我想到了"inling"这个短语,但我不是100%知道我想说的是什么意思?)

有没有人看到一种方法来转换rett . request . template到一个IQueryable,这样我就可以调用其他的WhereIsOwnedBy扩展方法吗?

还有其他建议吗?

在实体框架查询中使用c#函数

你可以保持你的语法并使它工作,但你需要在外部调用一个额外的方法IQueryable<>

技巧是手动替换IQueryable<>。表达式的副本,其中用相应的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();
}

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