我试图找出做我认为很容易的事情的最佳方法。我有一个名为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; }
}
这个类是从数据库模型生成的。
我还有另一个类,它添加了一个属性:
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,也就是我的总计算只存在于一个地方
- 支持排序和分页,并按照该顺序排序
- 不使用 .AsEnumerable 或 .AsArray 将整个数据表拉入内存
我真正希望找到的是一种在扩展的部分类中指定Linq to entities 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实例化的错误。
有了这三个部分类,现在您可以使用技巧,例如
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函数中进行了三行更改。这可能不是最安全的代码,而且可能会破坏其他东西,但它目前可用,并且我已经通知了作者有关缺陷的情况。
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
实际上,我可以十分肯定地说,这段代码可能会破坏一些东西,但目前它能够工作就已经足够好了。 :)