【.NET EF】Entity Framework 中如何配置动态的 Order By

634 阅读2分钟

由于在 EF 中,不管是什么 LINQ 表达式,都是只能写在外面,如果写在 LINQ 的 Lambda 表达式里面,编译器会直接编译成结果,导致 EF 无法拼出我们想要的 SQL。

比如我想达到这样的效果,按照 CREATED_DATE 进行升序排列:

var filteredCases = _dataModel.TEST_RESULT
    .Where(res => res.IS_ACTIVE && res.CUSTOMER_GUID == customerGuid);
filteredCases.OrderBy(res => res.CREATED_DATE);
return filteredCases.ToList();

动态配置要排序的列

如果现在要动态配置,让这个列名从外部传入。

直观的想法就是改成这样:

public List<TEST_RESULT> GetResults(string orderByPropName)
{
    var filteredCases = _dataModel.TEST_RESULT
        .Where(res => res.IS_ACTIVE && res.CUSTOMER_GUID == customerGuid);
    filteredCases.OrderBy(res => res.GetType().GetProperty(orderByPropName));
    return filteredCases.ToList();
}

显然,这样是错的

再加上 GetValue() 依然是错的。

我们就需要冷静下来思考一下,既然他需要一个完整的 Lambda 表达式来进行 SQL 的生成,那么我们需要传入的就是一个完整的 Lambda 表达式本身,而不是上面这种一半一半的四不像。

参考了 《C# 5 In a Nutshell》 之后,有了答案:

要组成我们自己的 Lambda 表达式(注意这边不是 Where 后面的 Predicate,那个可以用 LinqKit 包来快速生成,但这个不行!

直接上完整的代码:

public List<TEST_RESULT> GetResults(string orderByPropName)
{
    var filteredCases = _dataModel.TEST_RESULT
        .Where(res => res.IS_ACTIVE && res.CUSTOMER_GUID == customerGuid);
    
    // 第一步,定义一个 Parameter,就是 Lambda 表达式,箭头符号前面的东西
    ParameterExpression orderByParam = Expression.Parameter(typeof(TEST_RESULT));
    // 第二部,定义一个 Member,也就是 Lambda 表达式箭头符号后面的那个变量
    MemberExpression orderByMember = Expression.PropertyOrField(orderByParam, orderByPropName);
    // 第三步,通过这两个组成一个 Lambda 表达式对象
    LambdaExpression orderByLambda = Expression.Lambda(orderByMember, orderByParam);
	// 其实 Lambda 表达式对象是不能直接使用的,需要下面第四步:
    // 第四步,通过 Lambda 表达式对象,生成一个 MethodCall,这样才能用。
    Type[] methodCallArgTypes = { sourceQuery.ElementType, orderByLambda.Body.Type };
    MethodCallExpression methodCall = Expression.Call(typeof(Queryable), "OrderBy", methodCallArgTypes, sourceQuery.Expression, orderByLambda);
    // 第五步,把 Order By 的 SQL 拼接上去
    filteredCases = filteredCases.Provider.CreateQuery(methodCall) as IQueryable<TEST_RESULT>;
    // 这边 ToList 跑完之后,在 SQL Server Profiler 里面就可以看到带 Order By 的 SQL 啦。
    return filteredCases.ToList();
}

当然也可以把他封装成一个方法:

protected MethodCallExpression GetOrderByMethodCallExpression(IQueryable sourceQuery, Type sourceType, string orderByPropName, EnumOrderByType orderByType)
{
    if (orderByPropName != null && sourceType.GetProperty(orderByPropName) != null)
    {
        string orderByMethodName = orderByType == EnumOrderByType.ASC ? "OrderBy" : "OrderByDescending";

        ParameterExpression orderByParam = Expression.Parameter(sourceType);
        MemberExpression orderByMember = Expression.PropertyOrField(orderByParam, orderByPropName);
        LambdaExpression orderByLambda = Expression.Lambda(orderByMember, orderByParam);

        Type[] methodCallArgTypes = { sourceQuery.ElementType, orderByLambda.Body.Type };
        MethodCallExpression methodCall = Expression.Call(typeof(Queryable), orderByMethodName, methodCallArgTypes, sourceQuery.Expression, orderByLambda);
        return methodCall;
    }
    return null;
}

这样就完美了!