【问题标题】:Using a partial class property inside LINQ statement在 LINQ 语句中使用部分类属性
【发布时间】:2011-07-29 22:08:46
【问题描述】:

我正在尝试找出最好的方法来做我认为容易的事情。 我有一个名为 Line 的数据库模型,它代表发票中的一行。

大概是这样的:

public partial class Line 
{
    public Int32 Id { get; set; }
    public Invoice Invoice { get; set; }
    public String Name { get; set; }
    public String Description { get; set; }
    public Decimal Price { get; set; }
    public Int32 Quantity { get; set; }
}

这个类是从 db 模型生成的。
我有另一个类增加了一个属性:

public partial class Line
{
    public Decimal Total
    {
        get
        {
            return this.Price * this.Quantity
        }
    }
}

现在,通过我的客户控制器,我想做这样的事情:

var invoices = ( from c in _repository.Customers
                         where c.Id == id
                         from i in c.Invoices
                         select new InvoiceIndex
                         {
                             Id = i.Id,
                             CustomerName = i.Customer.Name,
                             Attention = i.Attention,
                             Total = i.Lines.Sum( l => l.Total ),
                             Posted = i.Created,
                             Salesman = i.Salesman.Name
                         }
        )

但我不能感谢臭名昭著的人

The specified type member 'Total' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.

重构它以使其正常工作的最佳方法是什么?

我已经尝试过 LinqKit、i.Lines.AsEnumerable(),并将 i.Lines 放入我的 InvoiceIndex 模型并让它计算视图的总和。

最后一个解决方案“有效”,但我无法对这些数据进行排序。我最终希望能够做到的是

var invoices = ( from c in _repository.Customers
                         ...
        ).OrderBy( i => i.Total )

我还想分页我的数据,所以我不想浪费时间将整个 c.Invoices 转换为带有 .AsEnumerable() 的列表

赏金

我知道这对某些人来说一定是个大问题。经过数小时的互联网搜索后,我得出的结论是没有得出任何令人满意的结论。然而,我相信对于那些尝试使用 ASP MVC 进行分页和排序的人来说,这一定是一个相当普遍的障碍。 我了解该属性无法映射到 sql,因此您无法在分页之前对其进行排序,但我正在寻找一种获得所需结果的方法。

完美解决方案的要求:

  • DRY,这意味着我的总计算将存在于 1 个位置
  • 支持排序和分页,并按顺序进行
  • 不使用 .AsEnumerable 或 .AsArray 将整个数据表拉入内存

我真的很高兴找到一种在我的扩展部分类中指定 Linq 到实体 SQL 的方法。但有人告诉我这是不可能的。请注意,解决方案不需要直接使用 Total 属性。根本不支持从 IQueryable 调用该属性。我正在寻找一种方法来通过不同的方法实现相同的结果,但同样简单和正交。

赏金的获胜者将是最后得票最高的解决方案,除非有人发布了一个完美的解决方案:)

除非您阅读答案,否则请忽略以下内容:

{1} 使用 Jacek 的解决方案,我更进一步,并使用 LinqKit 使属性可调用。这样,即使 .AsQueryable().Sum() 也包含在我们的部分类中。以下是我现在正在做的一些示例:

public partial class Line
{
    public static Expression<Func<Line, Decimal>> Total
    {
        get
        {
            return l => l.Price * l.Quantity;
        }
    }
}

public partial class Invoice
{
    public static Expression<Func<Invoice, Decimal>> Total
    {
        get
        {
            return i => i.Lines.Count > 0 ? i.Lines.AsQueryable().Sum( Line.Total ) : 0;
        }
    }
}

public partial class Customer
{
    public static Expression<Func<Customer, Decimal>> Balance
    {
        get
        {
            return c => c.Invoices.Count > 0 ? c.Invoices.AsQueryable().Sum( Invoice.Total ) : 0;
        }
    }
}

第一个技巧是 .Count 检查。这些是必需的,因为我猜您不能在空集上调用 .AsQueryable 。您收到关于 Null 实现的错误。

通过布置这 3 个部分类,您现在可以做一些技巧,例如

var customers = ( from c in _repository.Customers.AsExpandable()
                           select new CustomerIndex
                           {
                               Id = c.Id,
                               Name = c.Name,
                               Employee = c.Employee,
                               Balance = Customer.Balance.Invoke( c )
                           }
                    ).OrderBy( c => c.Balance ).ToPagedList( page - 1, PageSize );

var invoices = ( from i in _repository.Invoices.AsExpandable()
                         where i.CustomerId == Id 
                         select new InvoiceIndex
                        {
                            Id = i.Id,
                            Attention = i.Attention,
                            Memo = i.Memo,
                            Posted = i.Created,
                            CustomerName = i.Customer.Name,
                            Salesman = i.Salesman.Name,
                            Total = Invoice.Total.Invoke( i )
                        } )
                        .OrderBy( i => i.Total ).ToPagedList( page - 1, PageSize );

很酷。

有一个问题,LinqKit 不支持属性的调用,你会得到一个关于尝试将 PropertyExpression 转换为 LambaExpression 的错误。有两种方法可以解决这个问题。首先是像这样自己拉表情

var tmpBalance = Customer.Balance;
var customers = ( from c in _repository.Customers.AsExpandable()
                           select new CustomerIndex
                           {
                               Id = c.Id,
                               Name = c.Name,
                               Employee = c.Employee,
                               Balance = tmpBalance.Invoke( c )
                           }
                    ).OrderBy( c => c.Balance ).ToPagedList( page - 1, PageSize );

我认为这有点愚蠢。所以我修改了 LinqKit 以在遇到属性时提取 get{} 值。它对表达式的操作方式类似于反射,因此编译器不会为我们解析 Customer.Balance。我在 ExpressionExpander.cs 中对 TransformExpr 进行了 3 行更改。它可能不是最安全的代码,并且可能会破坏其他东西,但它现在可以工作,我已经通知了作者这个缺陷。

Expression TransformExpr (MemberExpression input)
{
        if( input.Member is System.Reflection.PropertyInfo )
        {
            return Visit( (Expression)( (System.Reflection.PropertyInfo)input.Member ).GetValue( null, null ) );
        }
        // Collapse captured outer variables
        if( input == null

事实上,我几乎可以保证这段代码会破坏一些东西,但它现在可以工作,这已经足够好了。 :)

【问题讨论】:

  • 我认为你的最后一次更新真的应该是它自己的答案。

标签: c# asp.net-mvc linq entity-framework asp.net-mvc-3


【解决方案1】:

还有另一种方式,它有点复杂,但可以让您封装这个逻辑。

public partial class Line
{
    public static Expression<Func<Line,Decimal>> TotalExpression
    {
        get
        {
            return l => l.Price * l.Quantity
        }
    }
}

然后将查询重写为

var invoices = ( from c in _repository.Customers
                     where c.Id == id
                     from i in c.Invoices
                     select new InvoiceIndex
                     {
                         Id = i.Id,
                         CustomerName = i.Customer.Name,
                         Attention = i.Attention,
                         Total = i.Lines.AsQueryable().Sum(Line.TotalExpression),
                         Posted = i.Created,
                         Salesman = i.Salesman.Name
                     }
               )

它对我有用,在服务器端执行查询并符合 DRY 规则。

【讨论】:

  • +1:还不错!我预计在 LINQ to Entities 投影中无法使用AsQueryable。但它确实有效(刚刚测试过)。会记住这个不错的“技巧”。
  • 我真的很喜欢这个解决方案。目前我正在与 LinqKit 搏斗,以尝试使该属性可调用。这会将这条线变成 Total = Invoice.Total.Invoke( i ) :D 目前 LinqKit 似乎不支持调用属性,但我认为它是可行的
  • 好吧,长话短说,我让 linqkit 工作了,这是一个非常好的解决方案 :) 在问题中的 {1} 下查看我的编辑以获取一些代码。
  • 是的,干得好。当我努力在 DevExpress XPO 中进行动态 Linq 查询时,我做了类似的事情。当然,如果您使用属性的属性等,这将中断。尽管如此,它还是让人很满意:)
【解决方案2】:

您的额外属性只是对模型数据的计算,但该属性不是 EF 可以自然翻译的东西。 SQL 完全能够执行相同的计算,并且 EF 可以翻译计算。如果您在查询中需要它,请使用它代替属性。

Total = i.Lines.Sum( l => l.Price * l.Quantity)

【讨论】:

  • 这行得通,但假设我在 5 个不同的地方执行此操作,一个月后我需要添加税金计算或运费,或者根据商品重量收取额外费用。我想要将“总计”计算封装在 Line 模型中。
  • 我想知道是否可以将我的代码中的类型计算通知 EF... 会说“在为该值构建 sql 命令时输出 [价格 * 数量]”那会很好
  • @Charles:不,不可能将自定义计算映射到属性。您的 linq-to-entities 查询只能包含映射(持久)属性。自定义计算可以包裹在model defined functions中,但更复杂。
【解决方案3】:

试试这样的:

var invoices =
    (from c in _repository.Customers
    where c.Id == id
    from i in c.Invoices
    select new 
    {
        Id = i.Id,
        CustomerName = i.Customer.Name,
        Attention = i.Attention,
        Lines = i.Lines,
        Posted = i.Created,
        Salesman = i.Salesman.Name
    })
    .ToArray()
    .Select (i =>  new InvoiceIndex
    {
        Id = i.Id,
        CustomerName = i.CustomerName,
        Attention = i.Attention,
        Total = i.Lines.Sum(l => l.Total),
        Posted = i.Posted,
        Salesman = i.Salesman
    });

基本上,这会在数据库中查询您需要的记录,将它们放入内存,然后在数据存在后进行求和。

【讨论】:

  • ToArray 实际上可以跳过。而是使用 AsEnumerable() 来隐藏 IQueryable ,这有助于保持流式传输性质,而不是缓冲/分配数组存储。
  • 现在这个很有趣。我没想过使用这样的匿名类型。我目前正在使用与 AsEnumerable 类似的东西,但据我了解,使用 ToArray 和 AsEnumerable 会导致 SQL 链结束,并且代码将在允许我进行总计算之前获取所有发票数据。这是不可取的,因为我正在对结果进行分页,并且一次只需要 10 张左右的发票。不过我可能不得不放弃这个要求,除了遵循安东尼的解决方案之外,我想不出一种可行的方法
  • @Charles - 你能不能在ToArray/AsEnumerable 调用之前进行分页?
  • 但是按 Id 排序的第 4 页与按 Total 排序的第 4 页将是不同的数据。我可以切换到分页然后排序,但是对于字母排序来说会有点奇怪。
  • 我认为我最终可能会扩展我的排序器,因此当它对表数据进行排序时,它使用一个简单的 .OrderBy,但如果它对计算字段进行排序,它首先使用 .AsEnumerable。这样一来,我的用户只有在对“坏”字段进行排序时才会受到性能影响。
【解决方案4】:

我认为解决此问题的最简单的方法是使用 DelegateDecompiler.EntityFramework(由 Alexander Zaytsev 制作

它是一个能够将委托或方法体反编译为其 lambda 表示的库。


说明:

假设我们有一个具有计算属性的类

class Employee
{
    [Computed]
    public string FullName
    {
        get { return FirstName + " " + LastName; }
    }

    public string LastName { get; set; }

    public string FirstName { get; set; }
}

您将按全名查询员工

var employees = (from employee in db.Employees
                 where employee.FullName == "Test User"
                 select employee).Decompile().ToList();

当您调用 .Decompile 方法时,它会将您的计算属性反编译为它们的底层表示,并且查询将变得类似于以下查询

var employees = (from employee in db.Employees
                 where (employee.FirstName + " " + employee.LastName)  == "Test User"
                 select employee).ToList();

如果你的类没有 [Computed] 属性,你可以使用 .Computed() 扩展方法..

var employees = (from employee in db.Employees
                 where employee.FullName.Computed() == "Test User"
                 select employee).ToList();

此外,您可以调用返回单个项目的方法(Any、Count、First、Single 等)以及其他方法,如下所示:

bool exists = db.Employees.Decompile().Any(employee => employee.FullName == "Test User");

同样,FullName 属性将被反编译:

bool exists = db.Employees.Any(employee => (employee.FirstName + " " + employee.LastName) == "Test User");

异步支持与 EntityFramework

DelegateDecompiler.EntityFramework 包提供了DecompileAsync 扩展方法,增加了对 EF 异步操作的支持。


另外

您可以在EF 中找到 8 种将一些属性值混合在一起的方法:

Computed Properties and Entity Framework。 (由 Dave Glick 撰写)

【讨论】:

  • 很高兴现在有一个真正的包来支持这个!
  • @Charles,是的,这是一个不错且有用的软件包。另外,我添加了对其他方式的回答的参考。
【解决方案5】:

虽然不是您的主题的答案,但它可能是您的问题的答案:

既然你好像愿意修改数据库,那何不在数据库中新建一个View呢,基本就是

create view TotalledLine as
select *, Total = (Price * Quantity)
from LineTable;

然后更改您的数据模型以使用 TotalledLine 而不是 LineTable?

毕竟,如果您的应用程序需要 Total,那么认为其他人可能需要 Total 并不是一件容易的事,而将这种类型的计算集中到数据库中是使用数据库的原因之一。

【讨论】:

  • 我确实考虑过一个视图,但我不认为实体框架可以与它们一起工作(非常容易)并且它们在数据库设计器中是不可设计的。
  • 查询和视图设计器用于维护视图,EF 可以很好地处理视图。您关心什么问题?
  • 计算也很简单,您可以触发更新,或实施最终确定几个值的“关闭订单”工作流程。 (有或没有任何 dbms 支持。)就个人而言,这些天我尽量保持与 db 无关,在过去的 10 年中,我看到的后端交换比我想承认的要多.. dbms 工件已经有些处理最昂贵的工件
【解决方案6】:

只是在这里添加一些我自己的想法。因为我是一个完美主义者,所以我不想将表格的全部内容拉到内存中,这样我就可以进行计算然后排序然后翻页。所以不知何故,表格需要知道总数是多少。

我的想法是在名为“CalcTotal”的行表中创建一个新字段,其中包含行的计算总数。每次更改行时都会根据 .Total 的值设置此值

这有两个优点。首先,我可以将查询更改为:

var invoices = ( from c in _repository.Customers
                     where c.Id == id
                     from i in c.Invoices
                     select new InvoiceIndex
                     {
                         Id = i.Id,
                         CustomerName = i.Customer.Name,
                         Attention = i.Attention,
                         Total = i.Lines.Sum( l => l.CalcTotal ),
                         Posted = i.Created,
                         Salesman = i.Salesman.Name
                     }
    )

排序和分页将起作用,因为它是一个 db 字段。我可以在我的控制器中进行检查( line.CalcTotal == line.Total )以检测任何汤姆愚弄。它确实会在我的存储库中造成一些开销,即当我去保存或创建新行时,我必须添加

line.CalcTotal = line.Total

但我认为这可能是值得的。

为什么要麻烦让 2 个属性具有相同的数据?

这是真的,我可以说

line.CalcTotal = line.Quantity * line.Price;

在我的存储库中。但这不会是非常正交的,如果需要对总行进行一些更改,编辑部分 Line 类比编辑 Line 存储库更有意义。

【讨论】:

  • 如果您准备更改数据库,为什么不在数据库中创建一个计算字段并将其设置为 storegenerated?
  • 我正在研究计算域,但找不到完美的解决方案。计算列需要是相关表中另一个计算列的总和 + 任何额外费用。另外我不相信有一种方法可以在 EF db 设计器中指定公式。但是,如果有人与他们一起发布了一个很酷的解决方案,我不会忽略它;)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-01
  • 1970-01-01
相关资源
最近更新 更多