【问题标题】:How to pass an Expression into Entity Framework LINQ query OrderBy clause如何将表达式传递到实体框架 LINQ 查询 OrderBy 子句
【发布时间】:2018-11-27 23:20:05
【问题描述】:

我有以下 LINQ 查询:

using (var context = new EUContext())
        {
            var tmp = context.Terms.Include(x => x.StudentCourses)
                .Where(x => x.StudentID == studentId && x.DepartmentID == departmentId)
                .OrderBy(x => x.AcademicYear)
                .ThenBy(x=> x.TermRegistered == "Fall" ? 1 :
                            x.TermRegistered == "Spring" ? 2 : 3));

            return tmp.ToList();
        }

我正在尝试移动 ThenBy 子句中的 OrdyBy 以清理代码。我正在尝试使用如下表达式:

private static Expression<Func<string, int>> TermsOrder(string x)
        {
            return (x == "Fall" ? 1 :
                    x == "Spring" ? 2 : 3);
        }

我的代码应该是这样的:

using (var context = new EUContext())
            {
                var tmp = context.Terms.Include(x => x.StudentCourses)
                    .Where(x => x.StudentID == studentId && x.DepartmentID == departmentId)
                    .OrderBy(x => x.AcademicYear)
                    .ThenBy(x=> TermsOrder(x.TermRegistered));

                return tmp.ToList();
            }

不幸的是,表达式不起作用,表达式主体中有一条很长的波浪线,并带有以下错误消息:

无法将类型 'int' 隐式转换为 'System.Linq.Expressions.Expression>

我做错了什么?这是我第一次尝试使用表达式,我知道由于不完全理解表达式的工作原理,我遗漏了一些明显的东西。

谢谢

【问题讨论】:

  • 我建议不要将此类数据作为字符串保存在数据库中。如果我是你,我会变成枚举而不是字符串。以后你可能会遇到更多的麻烦
  • 数据库是一个生产数据库,我正在编写代码来提供额外的功能。因此,目前无法更改数据库。
  • 看看return语句的类型——你返回什么?看看TermsOrder函数的类型——你想返回什么?

标签: c# linq lambda entity-framework-6 expression


【解决方案1】:

这并不像看起来那么简单。你需要结合Expressions 或构建Expressions 来生成你想要的东西,不幸的是C#在这方面没有提供很多帮助。

最简单的方法是使用LambdaExpression 组合的扩展方法。这取决于一些 Expression 扩展方法,用于将 Expression 中的一个 Expression 替换为另一个 Expression

public static class ExpressionExt {
    // Compose: f.Compose(g) => x => f(g(x))
    /// <summary>
    /// Composes two LambdaExpression into a new LambdaExpression: f.Compose(g) => x => f(g(x))
    /// </summary>
    /// <param name="fFn">The outer LambdaExpression.</param>
    /// <param name="gFn">The inner LambdaExpression.</param>
    /// <returns>LambdaExpression representing outer composed with inner</returns>
    public static Expression<Func<T, TResult>> Compose<T, TIntermediate, TResult>(this Expression<Func<TIntermediate, TResult>> fFn, Expression<Func<T, TIntermediate>> gFn) =>
        Expression.Lambda<Func<T, TResult>>(fFn.Body.Replace(fFn.Parameters[0], gFn.Body), gFn.Parameters[0]);    

    /// <summary>
    /// Replaces a sub-Expression with another Expression inside an Expression
    /// </summary>
    /// <param name="orig">The original Expression.</param>
    /// <param name="from">The from Expression.</param>
    /// <param name="to">The to Expression.</param>
    /// <returns>Expression with all occurrences of from replaced with to</returns>
    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);
}

/// <summary>
/// Standard ExpressionVisitor to replace an Expression with another in an Expression.
/// </summary>
public class ReplaceVisitor : ExpressionVisitor {
    readonly Expression from;
    readonly Expression to;

    public ReplaceVisitor(Expression from, Expression to) {
        this.from = from;
        this.to = to;
    }

    public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
}

现在您可以创建您的方法,该方法采用代表您要测试的字段的 lambda。它使用本地的LambdaExpression 作为最终结果的模板:

public static class Util {
    static Expression<Func<string, int>> TermOrderTemplateFn = p => (p == "Fall" ? 1 : p == "Spring" ? 2 : 3);
    public static Expression<Func<TRec, int>> TermsOrder<TRec>(Expression<Func<TRec, string>> selectorFn) =>
        TermOrderTemplateFn.Compose(selectorFn);
}

现在您可以在表达式中调用该方法,传入一个表示所需字段(或字段表达式)的 lambda 进行测试:

var tmp = context.Terms.Include(x => x.StudentCourses).AsQueryable()
                .Where(x => x.StudentID == studentId && x.DepartmentID == departmentId)
                .OrderBy(x => x.AcademicYear)
                .ThenBy(Util.TermsOrder<Term>(p => p.TermRegistered));

注意:我正在调用 context.Terms.First() Term 的类型,但您需要在调用 TermsOrder 时使用实际正确的类型名称。你也可以改用TermsOrder((Term p) =&gt; ...)

我可能更愿意创建一个特殊版本的ThenBy,这样您就可以使用类型推断来确定记录类型:

public static class EFExt {
    static Expression<Func<string, int>> TermThenOrderTemplateFn = p => (p == "Fall" ? 1 : p == "Spring" ? 2 : 3);
    public static IOrderedQueryable<T> ThenByTerm<T>(this IOrderedQueryable<T> src, Expression<Func<T, string>> selectorFn) =>
        src.ThenBy(TermThenOrderTemplateFn.Compose(selectorFn));
}

那么就可以直接使用了:

var tmp = context.Terms.Include(x => x.StudentCourses).AsQueryable()
                .Where(x => x.StudentID == studentId && x.DepartmentID == departmentId)
                .OrderBy(x => x.AcademicYear)
                .ThenByTerm(p => p.TermRegistered);

【讨论】:

  • 顺便说一句,我仍在研究如何创建一个有用的 Expressions 帮助库,这将使这些事情变得更容易。
  • (Expression&lt;Func&lt;T, int&gt;&gt;)Expression.Lambda而不是Expression.Lambda&lt;Func&lt;T, int&gt;&gt;有什么好处吗?
  • @AntonínLejsek 绝对没有。我不知道为什么存在这种重载时人们继续使用 cast 。可能是因为他们不知道 :)
  • @IvanStoev 我觉得这些库对于这个和类似问题来说太重了——例如,看看 LINQKit 在查询中需要什么。我喜欢Replace 这个名字。
  • @IvanStoev 更新了我的答案以使用函数式编程撰写术语并建议使用 IOrderedQueryable 扩展名。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-22
  • 1970-01-01
  • 1970-01-01
  • 2021-12-25
相关资源
最近更新 更多