将LINQ表达式作为参数传递给where子句

本文关键字:where 子句 参数传递 LINQ 表达式 | 更新日期: 2023-09-27 18:11:16

请在投票结束前仔细阅读该问题。那不是复制品。

我试图建立一个通用方法,返回类型T的实体列表加入到类型AuditLog的日志。下面是LINQ中的LEFT JOIN解释,我使用

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是不同的(它依赖于实体变量),例如对于一个特定的实体,它可以是

joinExpression = l => l.TableName == "SomeTable" && l.EntityId == entity.SomeTableId;

注意实体。SomeTableId以上。这就是我不能在查询开始之前初始化joinExpression的原因。如果joinExpression实际上依赖于作为查询本身一部分的"entity"变量,那么我如何将其作为参数传递?

将LINQ表达式作为参数传递给where子句

你的方法可能是这样的:

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();

我并没有真正看到这与我链接的副本有什么不同,在这两种情况下,您将查询作为表达式传递。显然,您需要传递带有所有依赖项的查询,因此您需要将entity值作为查询的一部分。

下面是一个独立的工作示例:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
using System.Linq.Expressions;
namespace SO24542133
{
    public class AuditLog
    {
        public int Id { get; set; }
        public string TableName { get; set; }
        public int? EntityId { get; set; }
        public string Text { get; set; } 
    }
    public class SomeEntity
    {
        public int Id { get; set; }
        public string Something { get; set; }
    }
    internal class AuditLogConfiguration : EntityTypeConfiguration<AuditLog>
    {
        public AuditLogConfiguration()
        {
            ToTable("dbo.AuditLog");
            HasKey(x => x.Id);
            Property(x => x.Id).HasColumnName("Id").IsRequired().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
            Property(x => x.TableName).HasColumnName("TableName").IsOptional().HasMaxLength(50);
            Property(x => x.EntityId).HasColumnName("EntityId").IsOptional();
            Property(x => x.Text).HasColumnName("Text").IsOptional();
        }
    }
    internal class SomeEntityConfiguration : EntityTypeConfiguration<SomeEntity>
    {
        public SomeEntityConfiguration()
        {
            ToTable("dbo.SomeEntity");
            HasKey(x => x.Id);
            Property(x => x.Id).HasColumnName("Id").IsRequired().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
            Property(x => x.Something).HasColumnName("Something").IsOptional();
        }
    }

    public interface IMyDbContext : IDisposable
    {
        IDbSet<AuditLog> AuditLogSet { get; set; }
        IDbSet<SomeEntity> SomeEntitySet { get; set; }
        int SaveChanges();
    }
    public class MyDbContext : DbContext, IMyDbContext
    {
        public IDbSet<AuditLog> AuditLogSet { get; set; }
        public IDbSet<SomeEntity> SomeEntitySet { get; set; }
        static MyDbContext()
        {
            Database.SetInitializer(new DropCreateDatabaseAlways<MyDbContext>());
        }
        public MyDbContext(string connectionString) : base(connectionString)
        {
        }
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Configurations.Add(new AuditLogConfiguration());
            modelBuilder.Configurations.Add(new SomeEntityConfiguration());
        }
    }

    class Program
    {
        private static void CreateTestData(MyDbContext context)
        {
            SomeEntity e1 = new SomeEntity { Something = "bla" };
            SomeEntity e2 = new SomeEntity { Something = "another bla" };
            SomeEntity e3 = new SomeEntity { Something = "third bla" };
            context.SomeEntitySet.Add(e1);
            context.SomeEntitySet.Add(e2);
            context.SomeEntitySet.Add(e3);
            context.SaveChanges();
            AuditLog a1 = new AuditLog { EntityId = e1.Id, TableName = "SomeEntity", Text = "abc" };
            AuditLog a2 = new AuditLog { EntityId = e1.Id, TableName = "AnotherTable", Text = "def" };
            AuditLog a3 = new AuditLog { EntityId = e1.Id, TableName = "SomeEntity", Text = "ghi" };
            AuditLog a4 = new AuditLog { EntityId = e2.Id, TableName = "SomeEntity", Text = "jkl" };
            context.AuditLogSet.Add(a1);
            context.AuditLogSet.Add(a2);
            context.AuditLogSet.Add(a3);
            context.AuditLogSet.Add(a4);
            context.SaveChanges();
        }
        static 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)
                });            
        }
        static void Main()
        {
            MyDbContext context = new MyDbContext("Data Source=(local);Initial Catalog=SO24542133;Integrated Security=True;");
            CreateTestData(context);
            Expression<Func<SomeEntity, IEnumerable<AuditLog>>> ddd = entity => context.AuditLogSet.Where(a => a.TableName == "SomeEntity" && entity.Id == a.EntityId).DefaultIfEmpty();
            var result = GetEntities(context.SomeEntitySet, ddd).ToList();
            // Examine results here
            result.ToString();
        }        
    }
}

并解决另一个关于DefaultIfEmpty的答案中提出的观点。对DefaultIfEmpty的调用只是表达式树上的一个节点,您最终在ddd变量中得到它。您不必在此表达式树中包含它,而是在您的GetEntites方法中将它动态地添加到作为参数接收的表达式树中。

编辑:

关于代码的其他问题,这个查询生成的sql不是最优的,这是正确的。特别糟糕的是,我们首先用SelectMany将连接变平,然后再用GroupBy将其变平。这没有多大意义。让我们看看如何改进它。首先,让我们摆脱这个动态的废话。我们的结果集项可以这样定义:

class QueryResultItem<T>
{
    public T Entity { get; set; }
    public IEnumerable<AuditLog> Logs { get; set; }
}

好。现在让我们重写EF查询,这样它就不会变平,然后按分组。让我们从简单的开始,并提出一个非泛型实现,稍后我们将对此进行改进。我们的查询可以像这样:

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);
}

我们显然需要能够从某处传递where表达式,所以让我们把这部分也分开到一个变量中:

static IQueryable<QueryResultItem<T>> GetEntities<T>(IDbSet<T> entitySet, IDbSet<AuditLog> auditLogSet, Expression<Func<AuditLog, T, bool>> whereTemplate) where T : class
{
    Expression<Func<AuditLog, bool>> whereExpression = null;                        
    Expression<Func<T, QueryResultItem<T>>> entityExpression = entity =>
        new QueryResultItem<T>
        {
            Entity = entity,
            Logs = auditLogSet.Where(whereExpression)
        };
    whereExpression = SubstituteSecondParameter(whereTemplate, entityExpression.Parameters[0]);
    return entitySet.Select(entityExpression);
}

所以现在表达式在一个单独的变量中,但我们也有机会做一些其他的改变。我们的方法现在又是泛型的,所以它可以接受任何实体。还要注意,我们传递的是一个where模板,但它有一个额外的泛型参数,它替代了我们所依赖的entity变量。由于类型不同,我们不能在表达式中直接使用该模板,因此需要某种方式将其转换为可以使用的表达式:神秘的SubstituteSecondParameter方法表示这一点。关于这段代码需要注意的最后一点是,我们将替换的结果赋值回给上面表达式中使用的变量。这能行吗?嗯,是的。表达式表示一个匿名方法,并通过它的优点提升局部变量和参数以形成闭包。如果你有ReSharper,你会注意到它警告你whereExpression变量在被解除后被修改。在大多数情况下,这是无意的,但在我们的例子中,这正是我们想要做的,将临时的whereExpression替换为实际的表达式。

下一步是考虑要传递给方法的内容。这很简单:

Expression<Func<AuditLog, SomeEntity, bool>> whereExpression2 = (l, entityParam) => l.TableName == "SomeEntity" && l.EntityId == entityParam.Id;

这将很好地解决。现在,最后一块拼图,我们如何将这个带有额外参数的表达式转换为包含这个参数的表达式。坏消息是你不能修改表达式树你必须从头开始重新构建它们。好消息是,马克能帮我们。首先,让我们定义一个简单的表达式访问者类,它基于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);
    }
}

我们所拥有的只是一个构造函数,它告诉我们用哪个节点替换哪个节点,以及一个重写来执行检查/替换。SubstituteSecondParameter也不是很复杂,它是两行:

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]);            
}

看签名,我们接受一个有两个形参和一个形参的表达式,返回一个只有一个形参的表达式。为此,我们创建了out访问者,将第二个参数传递给它为"To",并将方法参数参数传递给它为"from",然后构造一个新的Lambda表达式,该表达式只有一个参数,该参数来自原始表达式。到此结束。总的来说,这些是新的类/方法:

class QueryResultItem<T>
{
    public T Entity { get; set; }
    public IEnumerable<AuditLog> Logs { get; set; }
}
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);
    }
}

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]);            
}
static IQueryable<QueryResultItem<T>> GetEntities2<T>(IDbSet<T> entitySet, IDbSet<AuditLog> auditLogSet, Expression<Func<AuditLog, T, bool>> whereTemplate) where T : class
{
    Expression<Func<AuditLog, bool>> whereExpression = null;                        
    Expression<Func<T, QueryResultItem<T>>> entityExpression = entity =>
        new QueryResultItem<T>
        {
            Entity = entity,
            Logs = auditLogSet.Where(whereExpression)
        };
    whereExpression = SubstituteSecondParameter(whereTemplate, entityExpression.Parameters[0]);
    return entitySet.Select(entityExpression);
}

我们是这样称呼它们的:

Expression<Func<AuditLog, SomeEntity, bool>> whereExpression2 = (l, entityParam) => l.TableName == "SomeEntity" && l.EntityId == entityParam.Id;
var r2 = GetEntities2(context.SomeEntitySet, context.AuditLogSet, whereExpression2).ToList();

更好!

最后一件事。这是EF作为该查询的结果生成的SQL。正如你所看到的,它非常简单和可读(至少就EF生成的sql而言):

SELECT 
    [Project1].[Id] AS [Id], 
    [Project1].[Something] AS [Something], 
    [Project1].[C1] AS [C1], 
    [Project1].[Id1] AS [Id1], 
    [Project1].[TableName] AS [TableName], 
    [Project1].[EntityId] AS [EntityId], 
    [Project1].[Text] AS [Text]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Something] AS [Something], 
        [Extent2].[Id] AS [Id1], 
        [Extent2].[TableName] AS [TableName], 
        [Extent2].[EntityId] AS [EntityId], 
        [Extent2].[Text] AS [Text], 
        CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM  [dbo].[SomeEntity] AS [Extent1]
        LEFT OUTER JOIN [dbo].[AuditLog] AS [Extent2] ON (N'SomeEntity' = [Extent2].[TableName]) AND ([Extent2].[EntityId] = [Extent1].[Id])
    )  AS [Project1]
    ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC

所以你要做的是伪造一个Join的方式,使其易于通用。直接使用Join扩展方法而不是试图用Where子句来代替它是有意义的。不仅因为这就是Join的作用,而且因为你根本不能用其他方法来实现它。

LINQ中的Join方法需要三个Expression参数来完成它的工作:一对键选择器(连接的每一边一个)和一个选择表达式。你可以在方法中定义其中的两个(内部键选择器和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;
}

最后,调用

// 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());

好的部分是,在枚举查询之前,它实际上不会访问数据库。上述代码中直到First(...)调用之前的所有代码都是关于将IQueryable操作组合在一起的。

这是我能想到的最一般的了。不幸的是,它漏掉了一点:DefaultIfEmpty。通常我不会担心它,但我不知道在这里包含它的简单方法。也许其他人会指出一个。

我认为这是最好的方式:

joinExpression = (l, entityParam) => l.TableName == "SomeTable" && l.EntityId == entityParam.SomeTableId;

然后像这样改变你的位置:.Where(l => joinExpression(l, entity))

或者,像这样的操作也可以

joinExpression = entityParam => (l => l.TableName == "SomeTable" && l.EntityId == entityParam.SomeTableId);

,但在我看来更难读。

比我的其他答案更简单的解决方案是使用LinqKit。它封装了前面描述的大部分复杂性。使用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))
    });
}

然后把它做完。