我试图找出做我认为容易的事情的最好方法。 我有一个名为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,因此你不能在分页之前对它进行排序,但我正在寻找的是一种获得我想要的结果的方法。
完美解决方案的要求:
我真的很高兴能找到一种在扩展的分部类中指定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实现的错误。
通过这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
事实上,我几乎可以保证这段代码会破坏一些东西,但它现在可以运行,这已经足够了。 :)
答案 0 :(得分:22)
还有另一种方法,它有点复杂,但是你可以封装这种逻辑。
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 :(得分:4)
您的额外属性只是计算模型中的数据,但属性不是EF可以自然转换的。 SQL完全能够执行相同的计算,而EF 可以翻译计算。如果您在查询中需要它,请使用它代替属性。
Total = i.Lines.Sum( l => l.Price * l.Quantity)
答案 2 :(得分: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
});
基本上,这会向数据库查询所需的记录,将它们带入内存,然后在数据存在后进行求和。
答案 3 :(得分:3)
我认为最简单的方法来解决此问题的方法是使用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撰写)
答案 4 :(得分:1)
虽然不是您主题的答案,但它可能是您问题的答案:
由于您似乎愿意修改数据库,为什么不在数据库中创建一个基本上
的新视图create view TotalledLine as
select *, Total = (Price * Quantity)
from LineTable;
然后将您的数据模型更改为使用TotalledLine而不是LineTable?
毕竟,如果您的应用程序需要Total,那么认为其他人可能并不是一件容易的事,并且将这种类型的计算集中到数据库中是使用数据库的一个原因。
答案 5 :(得分:0)
在这里添加一些我自己的想法。因为我是一个完美主义者,我不想把表的全部内容都拉到内存中,所以我可以进行计算然后排序然后页面。所以表格需要知道总数是多少。
我在想的是在行表中创建一个名为'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存储库更有意义。