Использование частичного свойства класса внутри оператора LINQ - PullRequest
22 голосов
/ 30 июля 2011

Я пытаюсь найти лучший способ сделать то, что я думал, было бы легко. У меня есть модель базы данных под названием 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.InEnumerable ()

.

Bounty

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

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

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

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

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

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

* * тысяча сорок девять {1} Используя решение Яцека, я сделал еще один шаг вперед и сделал свойства доступными с помощью 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 для пустого набора. Вы получаете сообщение об ошибке материализации.

С этими тремя частичными классами вы можете выполнять такие трюки, как

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. Есть 2 способа обойти это. Во-первых, вытащить выражение себя так, как это

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 я сделал 3 строчные изменения в 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

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

Ответы [ 6 ]

22 голосов
/ 03 августа 2011

Есть еще один способ, который немного сложнее, но дает вам возможность инкапсулировать эту логику.

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

Это сработалодля меня выполняет запросы на стороне сервера и соответствует правилу СУХОЙ.

4 голосов
/ 30 июля 2011

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

Total = i.Lines.Sum( l => l.Price * l.Quantity)
3 голосов
/ 30 ноября 2016

Я думаю Самый простой способ для решения этой проблемы - использовать DelegateDecompiler.EntityFramework (make by Александр Зайцев )

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


Объяснить:

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

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, он декомпилирует ваши вычисленные свойства в их базовое представление, и запрос станет аналогичным следующемуquery

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.


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

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

Вычисленные свойства и Entity Framework .(написано Дейв Глик )

3 голосов
/ 30 июля 2011

Попробуйте что-то вроде этого:

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

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

1 голос
/ 05 августа 2011

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

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

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

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

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

0 голосов
/ 01 августа 2011

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

Я думал о том, чтобы создать новое поле в таблице строк под названием «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
                     }
    )

Сортировка и разбиение на страницы будут работать, потому что это поле базы данных. И я могу сделать проверку (line.CalcTotal == line.Total) в моем контроллере, чтобы обнаружить любое дурачество Тома. Это приводит к небольшим накладным расходам в моем хранилище, а именно, когда я иду, чтобы сохранить или создать новую строку, я должен добавить

line.CalcTotal = line.Total

но я думаю, что оно того стоит.

Зачем нам нужно иметь 2 свойства с одинаковыми данными?

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

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

в моем хранилище. Это не было бы слишком ортогональным, и если бы потребовалось какое-то изменение в итоговых значениях строк, имело бы гораздо больше смысла редактировать частичный класс Line, чем репозиторий Line.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...