你的方法可能是这样的:
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