C# 将LINQ表达式作为参数传递给where子句
在投票结束之前,请仔细阅读该问题。这不是重复的 我试图构建一个泛型方法,该方法返回与AuditLog类型日志关联的T类型实体列表。这里是我使用的LINQ中的左连接解释C# 将LINQ表达式作为参数传递给where子句,c#,.net,linq,entity-framework,linq-expressions,C#,.net,Linq,Entity Framework,Linq Expressions,在投票结束之前,请仔细阅读该问题。这不是重复的 我试图构建一个泛型方法,该方法返回与AuditLog类型日志关联的T类型实体列表。这里是我使用的LINQ中的左连接解释 var result = from entity in entitySet from auditLog in auditLogSet.Where(joinExpression).DefaultIfEmpty() select new { entity, auditLog }; r
var result = from entity in entitySet
from auditLog in auditLogSet.Where(joinExpression).DefaultIfEmpty()
select new { entity, auditLog };
return result.GroupBy(item => item.entity)
.Select(group => new
{
Entity = group.Key,
Logs = group.Where(i => i.auditLog != null).Select(i => i.auditLog)
});
问题出在joinExpression中。我想把它传递给WHERE子句,但是对于不同的具体类型T,它是不同的(它取决于entity变量),例如对于一个特定的实体,它可能是
joinExpression = l => l.TableName == "SomeTable" && l.EntityId == entity.SomeTableId;
请注意上面的entity.SomeTableId。这就是我无法在查询开始之前初始化joinExpression的原因。
如果joinExpression实际上依赖于“entity”变量,而“entity”变量是查询本身的一部分,那么如何将其作为参数传递呢?我认为这是最好的方法:
joinExpression = (l, entityParam) => l.TableName == "SomeTable" && l.EntityId == entityParam.SomeTableId;
然后像这样更改您的where:。where(l=>joinExpression(l,entity))
或者,类似的方法可能会奏效
joinExpression = entityParam => (l => l.TableName == "SomeTable" && l.EntityId == entityParam.SomeTableId);
但在我看来更难阅读。您的方法可能会阅读如下内容:
IQueryable<dynamic> GetEntities<T>(IDbSet<T> entitySet, Expression<Func<T, IEnumerable<AuditLog>>> joinExpression) where T : class
{
var result = entitySet.SelectMany(joinExpression,(entity, auditLog) => new {entity, auditLog});
return result.GroupBy(item => item.entity)
.Select(group => new
{
Entity = group.Key,
Logs = group.Where(i => i.auditLog != null).Select(i => i.auditLog)
});
}
Expression<Func<SomeEntity, IEnumerable<AuditLog>>> ddd = entity => auditLogSet.Where(a => a.TableName == "SomeEntity" && entity.Id == a.EntityId).DefaultIfEmpty();
var result = GetEntities(entitySet, ddd).ToList();
class QueryResultItem<T>
{
public T Entity { get; set; }
public IEnumerable<AuditLog> Logs { get; set; }
}
static IQueryable<QueryResultItem<SomeEntity>> GetEntities(IDbSet<SomeEntity> entitySet, IDbSet<AuditLog> auditLogSet)
{
return entitySet.Select(entity =>
new QueryResultItem<SomeEntity>
{
Entity = entity,
Logs = auditLogSet.Where(a => a.TableName == "SomeEntity" && entity.Id == a.EntityId)
});
}
static IQueryable<QueryResultItem<SomeEntity>> GetEntities(IDbSet<SomeEntity> entitySet, IDbSet<AuditLog> auditLogSet)
{
Expression<Func<SomeEntity, QueryResultItem<SomeEntity>>> entityExpression = entity =>
new QueryResultItem<SomeEntity>
{
Entity = entity,
Logs = auditLogSet.Where(a => a.TableName == "SomeEntity" && entity.Id == a.EntityId)
};
return entitySet.Select(entityExpression);
}
以及解决另一个答案中提出的关于DefaultIfEmpty
的问题。对DefaultIfEmpty
的调用只是表达式树上的一个节点,您最终会在ddd
变量中找到它。您不必将其包含在此表达式树中,而是将其包含在GetEntites
方法中,以添加到作为参数接收的表达式树中
编辑:
谈到代码的其他问题,这个查询生成的sql不是最优的,这是正确的。特别糟糕的是,我们首先使用SelectMany
展平连接,然后使用GroupBy
再次取消展平连接。这没有多大意义。让我们看看如何改进这一点。首先,让我们摆脱这种动态的胡说八道。我们的结果集项可以这样定义:
IQueryable<dynamic> GetEntities<T>(IDbSet<T> entitySet, Expression<Func<T, IEnumerable<AuditLog>>> joinExpression) where T : class
{
var result = entitySet.SelectMany(joinExpression,(entity, auditLog) => new {entity, auditLog});
return result.GroupBy(item => item.entity)
.Select(group => new
{
Entity = group.Key,
Logs = group.Where(i => i.auditLog != null).Select(i => i.auditLog)
});
}
Expression<Func<SomeEntity, IEnumerable<AuditLog>>> ddd = entity => auditLogSet.Where(a => a.TableName == "SomeEntity" && entity.Id == a.EntityId).DefaultIfEmpty();
var result = GetEntities(entitySet, ddd).ToList();
class QueryResultItem<T>
{
public T Entity { get; set; }
public IEnumerable<AuditLog> Logs { get; set; }
}
static IQueryable<QueryResultItem<SomeEntity>> GetEntities(IDbSet<SomeEntity> entitySet, IDbSet<AuditLog> auditLogSet)
{
return entitySet.Select(entity =>
new QueryResultItem<SomeEntity>
{
Entity = entity,
Logs = auditLogSet.Where(a => a.TableName == "SomeEntity" && entity.Id == a.EntityId)
});
}
static IQueryable<QueryResultItem<SomeEntity>> GetEntities(IDbSet<SomeEntity> entitySet, IDbSet<AuditLog> auditLogSet)
{
Expression<Func<SomeEntity, QueryResultItem<SomeEntity>>> entityExpression = entity =>
new QueryResultItem<SomeEntity>
{
Entity = entity,
Logs = auditLogSet.Where(a => a.TableName == "SomeEntity" && entity.Id == a.EntityId)
};
return entitySet.Select(entityExpression);
}
这会很顺利的。现在是谜题的最后一个部分,我们如何将这个带有额外参数的表达式转换为包含这个参数的表达式。坏消息是你不能修改表达式树,你必须从头开始重新构建它们。好消息是,在这里。首先,让我们定义一个简单的Expression Visitor类,它基于BCL中已经实现的内容,看起来很简单:
class ExpressionSubstitute : ExpressionVisitor
{
private readonly Expression _from;
private readonly Expression _to;
public ExpressionSubstitute(Expression from, Expression to)
{
_from = from;
_to = to;
}
public override Expression Visit(Expression node)
{
return node == _from ? _to : base.Visit(node);
}
}
我们所拥有的只是一个构造函数,它告诉我们用哪个节点替换哪个节点,以及一个执行检查/替换的重写。substituteSecond参数也不是很复杂,它是一个两行:
static Expression<Func<AuditLog, bool>> SubstituteSecondParameter<T>(Expression<Func<AuditLog, T, bool>> expression, ParameterExpression parameter)
{
ExpressionSubstitute swapParam = new ExpressionSubstitute(expression.Parameters[1], parameter);
return Expression.Lambda<Func<AuditLog, bool>>(swapParam.Visit(expression.Body), expression.Parameters[0]);
}
因此,您要做的是以一种使泛型变得容易的方式伪造一个Join
。直接使用Join
扩展方法而不是试图用Where
子句来伪造它是有意义的。这不仅仅是因为Join
就是这样做的,还因为你不能用另一种方式来做
LINQ中的Join
方法需要三个Expression
参数来完成其工作:一对键选择器(连接的每一侧一个)和一个select表达式。您可以在方法中定义其中两个(内部键选择器和select),只需传入最后一个键选择器
首先,需要为联接键定义一个类型。不能使用匿名类型,因为它们不使用。在这种情况下,应做到:
public class LogKey
{
public string TableName;
public int EntityId;
}
我们要删掉匿名报税表-你知道这是一件可怕的事情,对吗并返回可枚举的组合IQueryable
。它需要知道一些事情,比如使用什么连接以及它查询的数据列表,但可以简化为通用的
方法如下:
public IQueryable<IGrouping<T, LogEntry>> GetLogEntries<T>(
MyDataEntities context,
IQueryable<T> entities,
Expression<Func<T, LogKey>> outerKeySelector
)
{
// Join:
var query =
entities.Join(
context.auditLogSet,
outerKeySelector,
log => new LogKey { TableName = log.TableName, EntityId = log.EntityId },
(ent, log) => new { entity = ent, log = log }
);
// Grouping:
var group =
from pair in query
group pair.log by pair.entity into grp
select grp;
return group;
}
好的方面是,在枚举查询之前,它实际上不会对数据库造成任何影响。在上述代码中的第一次(…)
调用之前的所有代码都是关于将IQueryable
操作组合在一起的
这是我能想到的最普通的。不幸的是,它忽略了一点:DefaultIfEmpty
。通常我不会担心它,但我知道没有简单的方法将其包含在这里。也许其他人会指出一点。一个比我的另一个答案更简单的解决方案是使用。它封装了前面描述的大部分复杂性。使用LinqKit,您可以简单地编写:
static IQueryable<QueryResultItem<T>> GetEntities2<T>(IDbSet<T> entitySet, IDbSet<AuditLog> auditLogSet, Expression<Func<AuditLog, T, bool>> whereTemplate) where T : class
{
return entitySet.AsExpandable().Select(entity =>
new QueryResultItem<T>
{
Entity = entity,
Logs = auditLogSet.Where(x => whereTemplate.Invoke(x, entity))
});
}
静态IQueryable GetEntities2(IDbSet entitySet,IDbSet for it.可能与否重复,区别在于我的表达式依赖于“entity”变量(来自entitySet中的entity…)。我无法在查询之前创建joinExpression变量。表达式以何种方式依赖于该变量?假设您有一个GetJoinExpression(实体)
method——它看起来像什么?在最一般的形式下,您提出的问题没有很好的解决方案。EF必须能够将您的LINQ表达式编译为静态SQL查询。您认为它将如何实现这一壮举?即使EF成功,您认为生成的SQL将有效执行吗?因此,除非您的表达式是特殊的SQLrrow案例(如根据实体
类型更改“SomeTable”常量),答案可能是:这是不可能的,您的做法是错误的。
// get query for fetching logs grouped by entity:
var entLog = GetLogEntries(context, context.myEntities, e => new LogKey { TableName = "MyTableName", EntityId = (int)e.ID });
// get logs for entity with ID #2
var data = entLog.First(grp => grp.Key.ID == 2);
Console.WriteLine("ID {0}, {1} log entries", data.Key.ID, data.Count());
static IQueryable<QueryResultItem<T>> GetEntities2<T>(IDbSet<T> entitySet, IDbSet<AuditLog> auditLogSet, Expression<Func<AuditLog, T, bool>> whereTemplate) where T : class
{
return entitySet.AsExpandable().Select(entity =>
new QueryResultItem<T>
{
Entity = entity,
Logs = auditLogSet.Where(x => whereTemplate.Invoke(x, entity))
});
}