簡體   English   中英

在實體框架查詢中使用C#函數

[英]Using C# function in Entity Framework Query

在我的C#代碼中,我有2個WHERE查詢,這兩個查詢都可以調用IQueryable並將整個對象編譯為SQL,並且兩個查詢都有很多通用邏輯。

我相信這不是類似問題的重復: 在Entity Framework Query的Select子句中使用Function,因為在我的場景中,有問題的函數可以轉換為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會抱怨說,盡管它可以很好地處理SQL中的邏輯,但它不知道如何處理UserOwnsTemplate

AFAICT沒有解決此問題的好方法。 我認為我的選擇是:

  • UserOwnsTemplate轉換為UDF,即數據庫中定義的SQL函數。
    • 但是我無法從C#lamda創建UDF,我必須定義SQL,這會更加麻煩。
  • UserOwnsTemplate定義的Expression<Func<Template,bool>> UserOwnsTemplate為變量,然后使用Expression.AndAlso手工為DataReturn版本構建相關的Expression<Func<DataReturn ,bool>> ,並將兩個“子句”粘合在一起。
    • 元編程。 Ughhh。 我之前在另一個項目中已經做到這一點,這很容易做,而且噩夢要維護。
  • 與復制一起生活。
    • 除非SO另行建議,否則可能發生的情況。 ;)

誰能看到其他可用的選項嗎?

我可以做些什么來強迫EF將函數解析為SQL嗎? (我想到“內含”一詞,但我不是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();
}

問題是您的方法成為了表達式樹的一部分,而EF無法對其求值。 原則上,可以在觸發查詢之前評估表達式樹的各個部分。 看看Re-Linq: https ://relinq.codeplex.com/它有一個PartialEvaluatingExpressionTreeVisitor類,可以評估所有部分表達式樹,即它將找到您的方法,對其進行評估並注入實際的表達式樹。 這將付出一定的性能成本,但可能並不重要,您必須評估干凈的設計與性能。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM