Подтвердить что ты не робот

Использование частичного свойства класса внутри оператора LINQ

Я пытаюсь найти лучший способ сделать то, что, как я думал, будет легко. У меня есть модель базы данных под названием "Линия", которая представляет строку в счете-фактуре.

Это выглядит примерно так:

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()

Bounty

Я знаю, что для некоторых людей это должно быть несколько большая проблема. После нескольких часов очистки Интернета я пришел к выводу, что никакого счастливого заключения не было сделано. Тем не менее, я считаю, что это должно быть довольно распространенным препятствием для тех, кто пытается выполнить подкачку и сортировку с помощью ASP MVC. Я понимаю, что свойство не может быть сопоставлено с sql, и поэтому вы не можете сортировать его перед поисковым вызовом, но то, что я ищу, - это способ получить желаемый результат.

Требования к идеальному решению:

  • DRY, что означает, что мои общие вычисления будут существовать в 1-м месте.
  • Поддержка сортировки и подкачки и в этом порядке
  • Не вытащить всю таблицу данных в память с помощью .AsEnumerable или .AsArray

То, что мне было бы очень приятно найти, - это способ указать Linq для сущностей SQL в моем расширенном частичном классе. Но мне сказали, что это невозможно. Обратите внимание, что для решения не нужно напрямую использовать свойство Total. Вызов этого свойства из IQueryable вообще не поддерживается. Я ищу способ добиться того же результата с помощью другого метода, но в равной степени простой и ортогональный.

Победителем награды будет решение с наибольшим количеством голосов в конце, если кто-то не опубликует идеальное решение:)

Игнорировать ниже, пока вы не прочитаете ответ (ы):

{1} Используя решение Jacek, я сделал еще один шаг и сделал свойства invokable с помощью 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 для нас. Существует 3 изменения строки, которые я сделал для TransformExpr в ExpressionExpander.cs. Вероятно, это не самый безопасный код и может сломать другие вещи, но сейчас он работает, и я уведомил автора об этом недостатке.

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

На самом деле я в значительной степени гарантирую, что этот код сломает некоторые вещи, но он работает на данный момент, и это достаточно хорошо.:)

4b9b3361

Ответ 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.

Ответ 2

Ваше дополнительное свойство - это просто вычисление данных из модели, но свойство не является чем-то, что EF может естественно перевести. SQL вполне способен выполнить этот же расчет, и EF может перевести расчет. Используйте его вместо свойства, если оно вам понадобится в вашем запросе.

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

Ответ 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
    });

В основном это запрашивает базу данных для требуемых записей, выводит их в память, а затем суммирует данные, когда есть данные.

Ответ 4

Я думаю, что самый простой способ решить эту проблему с помощью DelegateDecompiler.EntityFramework (сделать Александр Зайцев)

Это библиотека, которая может декомпилировать делегат или тело метода в его лямбда-представление.


Объяснить:

У нас есть класс с вычисленным свойством

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");

Поддержка Async с EntityFramework

Пакет DelegateDecompiler.EntityFramework предоставляет метод расширения DecompileAsync, который добавляет поддержку операций EF Async.


Дополнительно

Вы можете найти 8 способов смешать некоторые значения свойств вместе в EF здесь:

Вычислимые свойства и платформа сущностей. (написанный Дейвом Глик)

Ответ 5

Не отвечая на ваш вопрос, это может быть ответ на вашу проблему:

Поскольку вы, похоже, хотите изменить базу данных, почему бы не создать новый вид в базе данных, который в основном

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

а затем изменить модель данных для использования TotalledLine вместо LineTable?

В конце концов, если вашему приложению требуется Total, не следует думать, что другие могут, и централизация такого типа вычислений в базе данных является одной из причин использования базы данных.

Ответ 6

Просто добавьте некоторые мои собственные мысли здесь. Поскольку я такой перфекционист, я не хочу вытащить все содержимое таблицы в память, чтобы я мог выполнять вычисления, а затем сортировать страницу. Итак, как-то таблица должна знать, что такое сумма.

То, о чем я думал, это создать новое поле в таблице строк, называемой CalcTotal, которая содержит вычисленное общее количество строк. Это значение будет устанавливаться каждый раз, когда строка будет изменена, в зависимости от значения .Total

Это имеет 2 преимущества. Во-первых, я могу изменить запрос к этому:

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

но я думаю, что это может стоить того.

Зачем переходить к проблеме наличия двух свойств, имеющих одни и те же данные?

Ну, это правда, я мог бы поставить

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

в моем репозитории. Однако это было бы не очень ортогонально, и если бы какое-то изменение в итоговых показателях линии должно было произойти, было бы гораздо разумнее отредактировать частичный класс Line, чем репозиторий Line.