【问题标题】:Pass LINQ expression as parameter to where clause将 LINQ 表达式作为参数传递给 where 子句
【发布时间】:2014-07-02 22:31:33
【问题描述】:

请在投票结束前仔细阅读问题。那不是重复的。

我正在尝试构建一个通用方法,该方法返回连接到 AuditLog 类型日志的 T 类型实体列表。这是我在 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(它取决于 entity 变量)是不同的,例如对于特定实体,它可能是

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

注意上面的 entity.SomeTableId。这就是我无法在查询开始之前初始化 joinExpression 的原因。 如果 joinExpression 实际上依赖于作为查询本身的一部分的“实体”变量,我如何将它作为参数传递?

【问题讨论】:

  • 不,不同之处在于我的表达式取决于“实体”变量(来自 entitySet 中的实体...)。我无法在查询之前创建 joinExpression 变量。
  • 表达式以何种方式依赖于该变量?想象一下你有一个GetJoinExpression(entity) 方法——它会是什么样子?
  • 在最一般的形式中,您提出的问题没有好的解决方案。 EF 必须能够将您的 LINQ 表达式编译为静态 SQL 查询。您认为它将如何实现这一壮举?即使 EF 成功了,你认为生成的 SQL 会高效执行吗?因此,除非您的表达式是特殊的狭义情况(例如根据 entity 类型更改“SomeTable”常量,否则答案可能是:这是不可能的,您这样做是错误的。

标签: c# .net linq entity-framework linq-expressions


【解决方案1】:

你的方法可能是这样的:

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 变量中找到它。您不必将其包含在此表达式树中,而是将 add it dynamically 包含在您的 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 变量。由于类型不同,我们不能直接在我们的表达式中使用这个模板,所以我们需要一些方法将它翻译成我们可以使用的 where 表达式:神秘的 SubstituteSecondParameter 方法表示这个。关于这段代码的最后一件事是,我们将替换的结果分配回我们上面在表达式中使用的变量。这行得通吗?嗯,是。该表达式表示一个匿名方法,并且由于它的优点,它提升了局部变量和参数以形成一个闭包。如果您有 ReSharper,您会注意到它会警告您 whereExpression 变量在提升后会被修改。在大多数情况下,这是无意的,但在我们的情况下,这正是我们想要做的,将临时 whereExpression 替换为真实的。

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

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

这会很顺利。现在是拼图的最后一块,我们如何将这个带有额外参数的表达式转换为其中包含这个参数的表达式。坏消息是你不能修改表达式树,你必须从头开始重新构建它们。好消息,Marc can help us 在这里。首先,让我们定义一个简单的 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);
    }
}

我们所拥有的只是一个构造函数,它告诉我们用什么节点替换哪个节点,以及一个覆盖来进行检查/替换。 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]);            
}

看签名,我们取一个有两个参数和一个参数的表达式,返回一个只有一个参数的表达式。为此,我们创建了访问者,将我们的第二个参数作为“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

【讨论】:

    【解决方案2】:

    因此,您要做的是伪造Join,以使其易于通用。直接使用Join 扩展方法而不是试图用Where 子句伪造它是有意义的。不仅因为这就是 Join 的用途,还因为您根本无法以其他方式通用。

    LINQ 中的Join 方法需要三个Expression 参数来完成其工作:一对键选择器(连接的每一侧一个)和一个选择表达式。您可以在方法中定义其中两个(内部键选择器和选择),然后传入最终的键选择器。

    首先,您需要为连接键定义一个类型。您不能使用匿名类型,因为它们不使用 .在这种情况下,应该这样做:

    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。通常我不会担心它,但我知道没有简单的方法可以将它包含在这里。也许其他人会指出一个。

    【讨论】:

      【解决方案3】:

      我认为这是最好的方式:

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

      然后像这样更改您的位置:.Where(l =&gt; joinExpression(l, entity))

      或者,这样的事情可能会起作用

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

      但在我看来更难阅读。

      【讨论】:

        【解决方案4】:

        比我的其他答案更简单的解决方案是使用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))
            });
        }
        

        并完成它。

        还有一个NuGet package

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-11-10
          • 2015-06-09
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多