在LINQ语句中使用部分类属性

时间:2011-07-29 22:08:46

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

我试图找出做我认为容易的事情的最好方法。 我有一个名为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,因此你不能在分页之前对它进行排序,但我正在寻找的是一种获得我想要的结果的方法。

完美解决方案的要求:

  • 干,意味着我的总计算将存在于一个地方
  • 支持排序和分页,并按顺序
  • 不要使用.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实现的错误。

通过这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

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

6 个答案:

答案 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存储库更有意义。