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

Iho*_*eka 0 .net c# 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)
                         });
Run Code Online (Sandbox Code Playgroud)

问题出在joinExpression中.我想将它传递给WHERE子句,但它对于不同的具体类型T(它依赖于实体变量)是不同的,例如对于特定实体它可能是

joinExpression = l => l.TableName == "SomeTable" && l.EntityId == entity.SomeTableId;
Run Code Online (Sandbox Code Playgroud)

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

And*_*ykh 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)
        });            
}
Run Code Online (Sandbox Code Playgroud)

然后你这样称呼它:

Expression<Func<SomeEntity, IEnumerable<AuditLog>>> ddd = entity => auditLogSet.Where(a => a.TableName == "SomeEntity" && entity.Id == a.EntityId).DefaultIfEmpty();
var result = GetEntities(entitySet, ddd).ToList();
Run Code Online (Sandbox Code Playgroud)

我并没有真正看到这与我链接的副本有何不同,在这两种情况下,您都将查询作为表达式传递.显然,您需要使用所有依赖项传递查询,因此您需要将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();
        }        
    }
}
Run Code Online (Sandbox Code Playgroud)

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

编辑:

要触及代码的其他问题,这是正确的,这个查询生成的sql不是最优的.特别糟糕的是,我们首先将连接SelectMany压平,然后再将其展开GroupBy.这没有多大意义.让我们看看我们如何改善这一点.首先,让我们摆脱这种动态的废话.我们的结果集项可以这样定义:

class QueryResultItem<T>
{
    public T Entity { get; set; }
    public IEnumerable<AuditLog> Logs { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

好.现在让我们重写我们的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)
        });
}
Run Code Online (Sandbox Code Playgroud)

干净整洁.现在让我们看看我们需要做些什么来使它适用于任何实体.首先,让表达式本身更容易操作,方法是将它拉入一个单独的变量,如下所示:

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);
}
Run Code Online (Sandbox Code Playgroud)

我们显然需要能够从某处传递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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

这将很好地解决.现在是拼图的最后一部分,我们如何将这个表达式与一个额外的参数转换为其中包含此参数的表达式.好消息是你无法修改表达式树,你必须从头开始重新构建它们.好消息,Marc可以帮助我们.首先,让我们定义一个简单的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我们所拥有的只是一个构造函数,它告诉我们用什么节点替换什么节点,以及执行检查/替换的覆盖.这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]);            
}
Run Code Online (Sandbox Code Playgroud)

查看签名,我们采用带有两个参数和一个参数的表达式,并返回一个只有一个参数的表达式.为此,我们创建了访问者,将第二个参数传递给"to",将方法参数参数传递为"from",然后构造一个新的Lambda Expression,它只有一个参数,我们从原始表达式中获取.最后得出结论.为了将我们的更改放在一起,这些是新的类/方法:

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);
}
Run Code Online (Sandbox Code Playgroud)

这就是我们称之为:

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

好多了!

最后一件事.这是由此查询生成的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
Run Code Online (Sandbox Code Playgroud)