指定 Func 发送到数据库

Specifying Func to send to database

我有一个通用方法,我想在其中指定要从中检索的 IQueryable、用作 ID 的字段以及 return 的字段名称。

但我收到错误:

Method '' has no supported translation to SQL.

如何正确指定下面的valueExpression,使其知道如何将表达式转换为SQL?我在这里做错了什么?

public void RunTest()
{
    Test<DocumentType>(ctx.Query<DocumentType>(), x => x.DocTypeID, x => x.DocType);
}

public void Test<TTable>(IQueryable<TTable> table, Func<TTable, int> idFunc, Expression<Func<TTable, string>> nameExpr)
{
    var intVal = 1; 
    Expression<Func<TTable, bool>> valueExpression = item => idFunc(item) == intVal;

    //errors on the Where() here.
    var dbName = table.Where(valueExpression).Select(nameExpr).SingleOrDefault();
    //make assertions
}

注意:intVal会在Test<>()方法中循环变化。我在这里简化了这个问题。

idFunc 需要是 Expression,而不是 Func,以便查询提供程序能够将其转换为 SQL.

一旦你有了它,你就可以使用下面的 Compose 方法将 id 选择器转换为一个谓词,将该值与你通过与另一个表达式组成的 ID 进行比较:

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}
public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

您现在可以写:

public void Test<TTable>(IQueryable<TTable> table,
    Expression<Func<TTable, int>> idSelector,
    Expression<Func<TTable, string>> nameSelector)
{
    int idValue = 1;
    var filter = idSelector.Compose(id => id == idValue);

    var dbName = table.Where(filter)
        .Select(nameSelector)
        .SingleOrDefault();
    //make assertions
}

上面的一个稍微简单的变体是

 public static void Test<TTable>(IQueryable<TTable> table, Expression<Func<TTable, int>> idFunc, Expression<Func<TTable, string>>> nameExpr)
 {
   var intVal = 1;

   var constant = Expression.Constant(intVal, typeof(int));
   var equalExpr = Expression.Equal(idFunc.Body, constant);
   var lambaWrap = Expression.Lambda<Func<TTable, bool>>(equalExpr, (ParameterExpression)((MemberExpression)idFunc.Body).Expression);

   var dbName = table.Where(lambaWrap).Select(nameExpr).SingleOrDefault();

   //assert
 }

结果:

这样做的问题在于,每次更改变量 intVal 时,都需要重新计算表达式。要解决这个问题,您需要 "grab" 外部变量。因为我不知道如何仅使用表达式来做到这一点,所以我通常将所有内容都包装在另一个 lambda 表达式中,returns 表达式本身并处理抓取的内部:

 public static void Test2<TTable>(IQueryable<TTable> table, Expression<Func<TTable, int>> idFunc, Expression<Func<TTable, string>>> nameExpr)
 {
   var intVal = 1;

   var variableParam = Expression.Parameter(typeof(int));
   var equalExpr = Expression.Equal(idFunc.Body, variableParam);
   var lambaWrap = Expression.Lambda<Func<TTable, bool>>(equalExpr, (ParameterExpression)((MemberExpression)idFunc.Body).Expression);
   var lambdaDoubleWrap = Expression.Lambda<Func<int, Expression<Func<TTable, bool>>>>(lambaWrap, variableParam).Compile();

   var dbName = table.Where(lambdaDoubleWrap(intVal)).Select(nameExpr).SingleOrDefault();

   //assert
 }

之前不变的 lambda 换行现在变成了: 问题是 "Param_0" 从何而来。这是第二个包装发挥作用的地方:

简单阅读:双重换行是创建我的函数表达式的函数。