C# 实体框架中LINQ投影的可重用计算(代码优先)

C# 实体框架中LINQ投影的可重用计算(代码优先),c#,linq,entity-framework,linq-to-entities,C#,Linq,Entity Framework,Linq To Entities,我的领域模型有很多复杂的财务数据,这些数据是对各种实体的多个属性进行相当复杂的计算的结果。我通常将这些属性作为[NotMapped]属性包含在适当的域模型中(我知道,我知道-关于在实体中放置业务逻辑有很多争论-出于实用性,它与AutoMapper配合得很好,让我定义可重用的DataAnnotations-关于这是否好的讨论不是我的问题) 只要我想具体化整个实体(以及任何其他依赖实体,通过.Include()LINQ调用或通过具体化后的其他查询),这就可以正常工作然后在查询后将这些属性映射到视图模

我的领域模型有很多复杂的财务数据,这些数据是对各种实体的多个属性进行相当复杂的计算的结果。我通常将这些属性作为
[NotMapped]
属性包含在适当的域模型中(我知道,我知道-关于在实体中放置业务逻辑有很多争论-出于实用性,它与AutoMapper配合得很好,让我定义可重用的
DataAnnotations
-关于这是否好的讨论不是我的问题)

只要我想具体化整个实体(以及任何其他依赖实体,通过
.Include()
LINQ调用或通过具体化后的其他查询),这就可以正常工作然后在查询后将这些属性映射到视图模型。当试图通过投影到视图模型而不是具体化整个实体来优化有问题的查询时,问题就出现了

考虑以下领域模型(明显简化):

如果我试图像这样直接进行项目:

List<CustomerViewModel> customers = MyContext.Customers
 .Select(x => new CustomerViewModel()
 {
  AccountValue = x.AccountValue
 })
 .ToList();
List<CustomerViewModel> customers = MyContext.Customers
 .WithAccountValue()
 .ToList();
private static Expression<Func<Project, double>> projectAverageEffectiveAreaSelector =
 proj => proj.Subprojects.Where(sp => sp.Area < 1000).Average(sp => sp.Area);
var proj1AndAea =
 ctx.Projects
  .AsExpressionProjectable()
  .Where(p => p.ID == 1)
  .Select(p => new 
  {  
   AEA = Utilities.projectAverageEffectiveAreaSelector.Project<double>() 
  });
因此我们可以得出结论,实际逻辑可以转换为SQL查询(也就是说,没有什么比从磁盘读取、访问外部变量等更奇怪的了)

所以这里有一个问题:是否有任何方法可以使逻辑在LINQ到实体的投影中可以转换为SQL重用

考虑到此计算可能在许多不同的视图模型中使用。将其复制到每个动作中的投影中既麻烦又容易出错。如果计算更改为包含乘数,该怎么办?我们必须手动查找并更改使用它的任何位置

我尝试过的一件事是将逻辑封装在
IQueryable
扩展中:

public static IQueryable<CustomerViewModel> WithAccountValue(
 this IQueryable<Customer> query)
{
 return query.Select(x => new CustomerViewModel()
 {
  AccountValue = x.Holdings.Sum(y => y.Quantity * y.Stock.Price)
 });
}
var employees = ctx.Employees
 .Select(x => new
 {
  FullName = x.FullName
 })
 .Decompile()
 .ToList();
带有AccountValue的公共静态IQueryable(
此查询(可查询)
{
返回query.Select(x=>newcustomerviewmodel()
{
AccountValue=x.Holdings.Sum(y=>y.Quantity*y.Stock.Price)
});
}
可以这样使用:

List<CustomerViewModel> customers = MyContext.Customers
 .Select(x => new CustomerViewModel()
 {
  AccountValue = x.AccountValue
 })
 .ToList();
List<CustomerViewModel> customers = MyContext.Customers
 .WithAccountValue()
 .ToList();
private static Expression<Func<Project, double>> projectAverageEffectiveAreaSelector =
 proj => proj.Subprojects.Where(sp => sp.Area < 1000).Average(sp => sp.Area);
var proj1AndAea =
 ctx.Projects
  .AsExpressionProjectable()
  .Where(p => p.ID == 1)
  .Select(p => new 
  {  
   AEA = Utilities.projectAverageEffectiveAreaSelector.Project<double>() 
  });
List customers=MyContext.customers
.WithAccountValue()
.ToList();
这在这样一个简单的人为设计的情况下已经足够好了,但它是不可组合的。因为扩展的结果是
IQueryable
而不是
IQueryable
,所以不能将它们链接在一起。如果我在一个视图模型中有两个这样的属性,一个在另一个视图模型中,另一个在第三个视图模型中,我将uld无法对所有三个视图模型使用相同的扩展-这将破坏整个目的。使用这种方法,要么全有,要么全无。每个视图模型必须具有完全相同的计算属性集(这种情况很少发生)


很抱歉这个冗长的问题。我更愿意提供尽可能多的细节,以确保大家理解这个问题,并有可能帮助其他人。我只是觉得我在这里遗漏了一些东西,这些东西会让所有这些都成为焦点。

您可以通过创建一个包含原始实体的类来封装逻辑然后创建投影到类的助手方法

例如,如果我们试图计算
员工
承包商
实体的税收,我们可以这样做:



在过去的几天里,我对此做了很多研究,因为这是构建高效实体框架查询的一个难点。我发现了几种不同的方法,基本上都可以归结为相同的基本概念。关键是采用计算属性(或方法),将其转换为查询提供程序知道如何转换为SQL的
表达式
,然后将其输入EF查询提供程序

我发现以下库/代码试图解决此问题:

LINQ表达式投影

此库允许您直接将可重用逻辑编写为
表达式
,然后提供转换以将该
表达式
转换为LINQ查询(因为查询不能直接使用
表达式
)。有趣的是,它将被查询提供程序转换回
表达式。
。可重用逻辑的声明如下所示:

List<CustomerViewModel> customers = MyContext.Customers
 .Select(x => new CustomerViewModel()
 {
  AccountValue = x.AccountValue
 })
 .ToList();
List<CustomerViewModel> customers = MyContext.Customers
 .WithAccountValue()
 .ToList();
private static Expression<Func<Project, double>> projectAverageEffectiveAreaSelector =
 proj => proj.Subprojects.Where(sp => sp.Area < 1000).Average(sp => sp.Area);
var proj1AndAea =
 ctx.Projects
  .AsExpressionProjectable()
  .Where(p => p.ID == 1)
  .Select(p => new 
  {  
   AEA = Utilities.projectAverageEffectiveAreaSelector.Project<double>() 
  });
这也可以很容易地扩展,这是一个很好的方法。例如,我将其设置为查找
NotMapped
数据注释,而不必显式使用
ComputedAttribute

设置实体后,只需使用
.Decompile()
扩展名触发反编译:

public static IQueryable<CustomerViewModel> WithAccountValue(
 this IQueryable<Customer> query)
{
 return query.Select(x => new CustomerViewModel()
 {
  AccountValue = x.Holdings.Sum(y => y.Quantity * y.Stock.Price)
 });
}
var employees = ctx.Employees
 .Select(x => new
 {
  FullName = x.FullName
 })
 .Decompile()
 .ToList();

如果您只需要为每个投影使用一个计算属性,这种方法就足够了。但是,如果您需要在每个投影中合成多个计算属性,这种方法就会出现问题。在这种情况下,您最终会遇到一堆乱七八糟的包装,这些包装都必须分解到最终的投影对象中。这也会导致它非常脆弱,因为在链的末尾添加一个额外的计算意味着所有的最终属性都需要进行调整,以考虑额外的包装。最终的赋值是:
x.wrapper.wrapper.wrapper.Entity.Name
我看不出这是如何在不同的
Entity
类型之间重用的。您有很强的所有示例中的引用。与其说它可以跨实体类型重用,不如说它在单个投影中具有多个计算属性。例如,在上面的
Employee
类中,假设我们还有一个
Initials
属性,它是
LastName[0]+FirstName[0]
。如果我只想用
全名
做一个投影,然后用
全名
首字母
做另一个投影,我要么需要每个投影都有一个包装器,要么使用您的方法链接特定于属性的包装器。这在simp中很好