如何在提供 IQueryable 输出的 linq 查询中使用 Func
How to use Func in a linq query that provide an IQueryable output
我向 return 我的服务中的 IQueryable 提供了以下查询(简化版):
var query =
(from item in _entityRepository.DbSet()
where
MyCondition
orderby Entity.EntityID descending
select new DTOModel
{
Id = Entity.EntityID,
...,
//My problem is here, when I trying to call a function into linq query:
//Size = Entity.IsPersian ? (Entity.EntitySize.ConvertNumbersToPersian()) : (Entity.EntitySize)
//Solution (1):
//Size = ConvertMethod1(Entity)
//Solution (2):
//Size = ConvertMethod2(Entity)
});
根据我的查询,我的服务中还有以下代码 class:
//Corresponding to solution (1):
Func<Entity, string> ConvertMethod1 = p => (p.IsPersian ? p.EntitySize.ConvertNumbersToPersian() : p.EntitySize);
//Corresponding to solution (2):
Expression<Func<Entity, string>> ConvertMethod2 = (p) => (p.IsPersian ? p.EntitySize.ConvertNumbersToPersian() : p.EntitySize);
而且我看到了以下错误:
对应解决方案 (1) 的生成错误:
LINQ to Entities 不支持 LINQ 表达式节点类型 'Invoke'。
对应解决方案(2)产生的错误:
编译错误:
需要方法、委托或事件
非常感谢任何高级帮助。
据我了解,这是因为 IQueryable 生成 SQL 语法并针对数据库进行查询,就像典型的 SQL 命令一样。它无法将 func 解析为 SQL 命令,因此无法以这种方式完成。您将不得不查询所需的最少数据量,强制查询以便获得非 IQueryable 结果,然后使用 func 再次查询它。如果需要 func 来防止一次从数据库中查询太多数据,那么我建议以不同的方式解决它,或者可能添加自定义数据库方法。有很多解决方法,但最重要的是 IQueryable 的构建方式不同,目的是防止对数据库进行多次调用并尽可能优化调用。我不确定你是否知道如何写 SQL;如果你这样做,那么考虑 IQueryable 调用在逻辑上像 SQL 一样工作(因为基本上它是)。希望这对您有所帮助。
这真的要归功于 IQueryable<>
结合 ORM 公开的 leaky abstraction。
第一次尝试在内存中执行时会成功;但是,使用 ORM 时情况并非如此。 原因您的第一个代码不能与 LINQ to entities 一起使用,因为 Func<>
是 compiled 代码。它不代表可以轻松转换为 SQL.
的表达式树
第二次尝试是自然的尝试解决方案,但由于您的代码有点神奇地转换为表达式树而失败。当您编写 select 时,您并不是在针对 Expression
对象进行编码。但是当你编译代码时; C# 会自动将其转换为表达式树。遗憾的是,无法轻松地将 实际 Expression
项目加入组合。
您需要的是:
- 用于获取表达式引用的占位符函数
- 表达式树重写器,如果您要将查询发送到 ORM。
你最终的查询是这样的:
Expression<Func<Person, int>> personIdSelector = person => person.PersonID;
var query = Persons
.Select(p =>
new {
a = personIdSelector.Inline(p)
})
.ApplyInlines();
使用以下表达式助手:
public static class ExpressionExtensions
{
public static TT Inline<T, TT>(this Expression<Func<T, TT>> expression, T item)
{
// This will only execute while run in memory.
// LINQ to Entities / EntityFramework will never invoke this
return expression.Compile()(item);
}
public static IQueryable<T> ApplyInlines<T>(this IQueryable<T> expression)
{
var finalExpression = expression.Expression.ApplyInlines().InlineInvokes();
var transformedQuery = expression.Provider.CreateQuery<T>(finalExpression);
return transformedQuery;
}
public static Expression ApplyInlines(this Expression expression) {
return new ExpressionInliner().Visit(expression);
}
private class ExpressionInliner : ExpressionVisitor
{
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.Name == "Inline" && node.Method.DeclaringType == typeof(ExpressionExtensions))
{
var expressionValue = (Expression)Expression.Lambda(node.Arguments[0]).Compile().DynamicInvoke();
var arg = node.Arguments[1];
var res = Expression.Invoke(expressionValue, arg);
return res;
}
return base.VisitMethodCall(node);
}
}
}
// https://codereview.stackexchange.com/questions/116530/in-lining-invocationexpressions/147357#147357
public static class ExpressionHelpers
{
public static TExpressionType InlineInvokes<TExpressionType>(this TExpressionType expression)
where TExpressionType : Expression
{
return (TExpressionType)new InvokeInliner().Inline(expression);
}
public static Expression InlineInvokes(this InvocationExpression expression)
{
return new InvokeInliner().Inline(expression);
}
public class InvokeInliner : ExpressionVisitor
{
private Stack<Dictionary<ParameterExpression, Expression>> _context = new Stack<Dictionary<ParameterExpression, Expression>>();
public Expression Inline(Expression expression)
{
return Visit(expression);
}
protected override Expression VisitInvocation(InvocationExpression e)
{
var callingLambda = e.Expression as LambdaExpression;
if (callingLambda == null)
return base.VisitInvocation(e);
var currentMapping = new Dictionary<ParameterExpression, Expression>();
for (var i = 0; i < e.Arguments.Count; i++)
{
var argument = Visit(e.Arguments[i]);
var parameter = callingLambda.Parameters[i];
if (parameter != argument)
currentMapping.Add(parameter, argument);
}
if (_context.Count > 0)
{
var existingContext = _context.Peek();
foreach (var kvp in existingContext)
{
if (!currentMapping.ContainsKey(kvp.Key))
currentMapping[kvp.Key] = kvp.Value;
}
}
_context.Push(currentMapping);
var result = Visit(callingLambda.Body);
_context.Pop();
return result;
}
protected override Expression VisitParameter(ParameterExpression e)
{
if (_context.Count > 0)
{
var currentMapping = _context.Peek();
if (currentMapping.ContainsKey(e))
return currentMapping[e];
}
return e;
}
}
}
这将允许您在进入 ORM 之前重写表达式树,从而允许您将表达式直接内联到树中。
我向 return 我的服务中的 IQueryable 提供了以下查询(简化版):
var query =
(from item in _entityRepository.DbSet()
where
MyCondition
orderby Entity.EntityID descending
select new DTOModel
{
Id = Entity.EntityID,
...,
//My problem is here, when I trying to call a function into linq query:
//Size = Entity.IsPersian ? (Entity.EntitySize.ConvertNumbersToPersian()) : (Entity.EntitySize)
//Solution (1):
//Size = ConvertMethod1(Entity)
//Solution (2):
//Size = ConvertMethod2(Entity)
});
根据我的查询,我的服务中还有以下代码 class:
//Corresponding to solution (1):
Func<Entity, string> ConvertMethod1 = p => (p.IsPersian ? p.EntitySize.ConvertNumbersToPersian() : p.EntitySize);
//Corresponding to solution (2):
Expression<Func<Entity, string>> ConvertMethod2 = (p) => (p.IsPersian ? p.EntitySize.ConvertNumbersToPersian() : p.EntitySize);
而且我看到了以下错误:
对应解决方案 (1) 的生成错误:
LINQ to Entities 不支持 LINQ 表达式节点类型 'Invoke'。
对应解决方案(2)产生的错误:
编译错误: 需要方法、委托或事件
非常感谢任何高级帮助。
据我了解,这是因为 IQueryable 生成 SQL 语法并针对数据库进行查询,就像典型的 SQL 命令一样。它无法将 func 解析为 SQL 命令,因此无法以这种方式完成。您将不得不查询所需的最少数据量,强制查询以便获得非 IQueryable 结果,然后使用 func 再次查询它。如果需要 func 来防止一次从数据库中查询太多数据,那么我建议以不同的方式解决它,或者可能添加自定义数据库方法。有很多解决方法,但最重要的是 IQueryable 的构建方式不同,目的是防止对数据库进行多次调用并尽可能优化调用。我不确定你是否知道如何写 SQL;如果你这样做,那么考虑 IQueryable 调用在逻辑上像 SQL 一样工作(因为基本上它是)。希望这对您有所帮助。
这真的要归功于 IQueryable<>
结合 ORM 公开的 leaky abstraction。
第一次尝试在内存中执行时会成功;但是,使用 ORM 时情况并非如此。 原因您的第一个代码不能与 LINQ to entities 一起使用,因为 Func<>
是 compiled 代码。它不代表可以轻松转换为 SQL.
第二次尝试是自然的尝试解决方案,但由于您的代码有点神奇地转换为表达式树而失败。当您编写 select 时,您并不是在针对 Expression
对象进行编码。但是当你编译代码时; C# 会自动将其转换为表达式树。遗憾的是,无法轻松地将 实际 Expression
项目加入组合。
您需要的是:
- 用于获取表达式引用的占位符函数
- 表达式树重写器,如果您要将查询发送到 ORM。
你最终的查询是这样的:
Expression<Func<Person, int>> personIdSelector = person => person.PersonID;
var query = Persons
.Select(p =>
new {
a = personIdSelector.Inline(p)
})
.ApplyInlines();
使用以下表达式助手:
public static class ExpressionExtensions
{
public static TT Inline<T, TT>(this Expression<Func<T, TT>> expression, T item)
{
// This will only execute while run in memory.
// LINQ to Entities / EntityFramework will never invoke this
return expression.Compile()(item);
}
public static IQueryable<T> ApplyInlines<T>(this IQueryable<T> expression)
{
var finalExpression = expression.Expression.ApplyInlines().InlineInvokes();
var transformedQuery = expression.Provider.CreateQuery<T>(finalExpression);
return transformedQuery;
}
public static Expression ApplyInlines(this Expression expression) {
return new ExpressionInliner().Visit(expression);
}
private class ExpressionInliner : ExpressionVisitor
{
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.Name == "Inline" && node.Method.DeclaringType == typeof(ExpressionExtensions))
{
var expressionValue = (Expression)Expression.Lambda(node.Arguments[0]).Compile().DynamicInvoke();
var arg = node.Arguments[1];
var res = Expression.Invoke(expressionValue, arg);
return res;
}
return base.VisitMethodCall(node);
}
}
}
// https://codereview.stackexchange.com/questions/116530/in-lining-invocationexpressions/147357#147357
public static class ExpressionHelpers
{
public static TExpressionType InlineInvokes<TExpressionType>(this TExpressionType expression)
where TExpressionType : Expression
{
return (TExpressionType)new InvokeInliner().Inline(expression);
}
public static Expression InlineInvokes(this InvocationExpression expression)
{
return new InvokeInliner().Inline(expression);
}
public class InvokeInliner : ExpressionVisitor
{
private Stack<Dictionary<ParameterExpression, Expression>> _context = new Stack<Dictionary<ParameterExpression, Expression>>();
public Expression Inline(Expression expression)
{
return Visit(expression);
}
protected override Expression VisitInvocation(InvocationExpression e)
{
var callingLambda = e.Expression as LambdaExpression;
if (callingLambda == null)
return base.VisitInvocation(e);
var currentMapping = new Dictionary<ParameterExpression, Expression>();
for (var i = 0; i < e.Arguments.Count; i++)
{
var argument = Visit(e.Arguments[i]);
var parameter = callingLambda.Parameters[i];
if (parameter != argument)
currentMapping.Add(parameter, argument);
}
if (_context.Count > 0)
{
var existingContext = _context.Peek();
foreach (var kvp in existingContext)
{
if (!currentMapping.ContainsKey(kvp.Key))
currentMapping[kvp.Key] = kvp.Value;
}
}
_context.Push(currentMapping);
var result = Visit(callingLambda.Body);
_context.Pop();
return result;
}
protected override Expression VisitParameter(ParameterExpression e)
{
if (_context.Count > 0)
{
var currentMapping = _context.Peek();
if (currentMapping.ContainsKey(e))
return currentMapping[e];
}
return e;
}
}
}
这将允许您在进入 ORM 之前重写表达式树,从而允许您将表达式直接内联到树中。