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

时间:2014-07-02 22:31:33

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

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

我正在尝试构建一个泛型方法,该方法返回类型为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;

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

4 个答案:

答案 0 :(得分:7)

你的方法可能是这样的:

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方法中的add it dynamically包含在作为参数接收的表达式树中。

修改

要触及代码的其他问题,这是正确的,这个查询生成的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;

这将很好地解决。现在是拼图的最后一部分,我们如何将这个表达式与一个额外的参数转换为其中包含此参数的表达式。好消息是你无法修改表达式树,你必须从头开始重新构建它们。好消息,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

答案 1 :(得分:1)

所以你要做的就是伪造一个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。通常我不会担心它,但我知道在这里包含它并不是一个简单的方法。也许其他人会指出一个。

答案 2 :(得分:0)

我认为这是最好的方式:

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

但在我看来更难阅读。

答案 3 :(得分:0)

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