Usando una propiedad de clase parcial dentro de la statement LINQ

Estoy tratando de encontrar la mejor manera de hacer lo que pensé que sería fácil. Tengo un modelo de base de datos llamado Línea que representa una línea en una factura.

Se ve más o menos así:

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

Esta clase se genera a partir del modelo db.
Tengo otra clase que agrega una propiedad más:

 public partial class Line { public Decimal Total { get { return this.Price * this.Quantity } } } 

Ahora, desde el controlador de mi cliente quiero hacer algo como esto:

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

Pero no puedo gracias a los infames.

 The specified type member 'Total' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported. 

¿Cuál es la mejor manera de refactorizar esto para que funcione?

He probado LinqKit, i.Lines.AsEnumerable (), y puse i.Lines en mi modelo InvoiceIndex y le pido que calcule la sum de la vista.

La última solución ‘funciona’ pero no puedo ordenar esos datos. Lo que quiero poder hacer al final es

 var invoices = ( from c in _repository.Customers ... ).OrderBy( i => i.Total ) 

También quiero hacer una página de mis datos, por lo que no quiero perder el tiempo convirtiendo todas las facturas en una lista con .AsEnumerable ()

Generosidad

Sé que esto debe ser un gran problema para algunas personas. Después de horas de recorrer Internet, he llegado a la conclusión de que no se ha llegado a ninguna conclusión feliz. Sin embargo, creo que este debe ser un obstáculo bastante común para aquellos que están tratando de hacer paginación y clasificación con ASP MVC. Entiendo que la propiedad no se puede asignar a sql y, por lo tanto, no se puede clasificar antes de la paginación, pero lo que estoy buscando es una forma de obtener el resultado deseado.

Requisitos para una solución perfecta:

  • SECO, lo que significa que mis cálculos totales existirían en 1 lugar
  • Soporta tanto la clasificación como la paginación, y en ese orden
  • No tire de toda la tabla de datos a la memoria con .AsEnumerable o .AsArray

Lo que me encantaría encontrar es una forma de especificar el Linq a las entidades SQL en mi clase parcial extendida. Pero me han dicho que esto no es posible. Tenga en cuenta que una solución no necesita usar directamente la propiedad Total. Llamar a esa propiedad desde IQueryable no es compatible en absoluto. Estoy buscando una manera de lograr el mismo resultado a través de un método diferente, pero igualmente simple y ortogonal.

El ganador de la recompensa será la solución con los votos más altos al final, a menos que alguien publique una solución perfecta 🙂

Ignora a continuación hasta que leas la (s) respuesta (s):

{1} Usando la solución de Jacek, fui un paso más allá e hice que las propiedades fueran invocables usando LinqKit. De esta manera, incluso el .AsQueryable (). Sum () se incluye en nuestras clases parciales. Aquí hay algunos ejemplos de lo que estoy haciendo ahora:

 public partial class Line { public static Expression<Func> Total { get { return l => l.Price * l.Quantity; } } } public partial class Invoice { public static Expression<Func> Total { get { return i => i.Lines.Count > 0 ? i.Lines.AsQueryable().Sum( Line.Total ) : 0; } } } public partial class Customer { public static Expression<Func> Balance { get { return c => c.Invoices.Count > 0 ? c.Invoices.AsQueryable().Sum( Invoice.Total ) : 0; } } } 

El primer truco fueron los cheques .Count. Esas son necesarias porque supongo que no puedes llamar a .AsQueryable en un conjunto vacío. Recibes un error sobre materialización nula.

Con estas 3 clases parciales diseñadas ahora puedes hacer trucos como

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

Muy genial.

Hay un problema, LinqKit no admite la invocación de propiedades, obtendrá un error al intentar convertir PropertyExpression en LambaExpression. Hay 2 maneras de evitar esto. En primer lugar es tirar de la expresión a ti mismo así.

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

que pensé que era un poco tonto Así que modifiqué LinqKit para extraer el valor de obtener {} cuando encuentra una propiedad. La forma en que opera en la expresión es similar a la reflexión, por lo que no es como que el comstackdor va a resolver el equilibrio de Customer para nosotros. Hay un cambio de 3 líneas que hice en TransformExpr en ExpressionExpander.cs. Probablemente no sea el código más seguro y podría romper otras cosas, pero funciona por ahora y le notifiqué al autor sobre la deficiencia.

 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 

De hecho, casi garantizo que este código romperá algunas cosas, pero funciona en este momento y eso es lo suficientemente bueno. 🙂

Hay otra forma, que es un poco más compleja, pero le da la capacidad de encapsular esta lógica.

 public partial class Line { public static Expression> TotalExpression { get { return l => l.Price * l.Quantity } } } 

Luego reescribe la consulta a

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

Funcionó para mí, realiza consultas del lado del servidor y cumple con la regla DRY.

Su propiedad adicional es simplemente un cálculo de los datos del modelo, pero la propiedad no es algo que EF pueda traducir naturalmente. SQL es perfectamente capaz de realizar este mismo cálculo, y EF puede traducir el cálculo . Úselo en lugar de la propiedad si lo necesita en su consulta.

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

Intenta algo como esto:

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

Básicamente, esto consulta la base de datos de los registros que necesita, los lleva a la memoria y luego los sum una vez que los datos están allí.

Creo que la forma más fácil de resolver este problema es usar DelegateDecompiler.EntityFramework (hecho por Alexander Zaytsev )

Es una biblioteca que puede descomstackr un delegado o un cuerpo de método a su representación lambda.


Explique:

Asume que tenemos una clase con una propiedad computada.

 class Employee { [Computed] public string FullName { get { return FirstName + " " + LastName; } } public string LastName { get; set; } public string FirstName { get; set; } } 

Y usted va a consultar a los empleados por sus nombres completos

 var employees = (from employee in db.Employees where employee.FullName == "Test User" select employee).Decompile().ToList(); 

Cuando llama al método .Decompile, descomstack sus propiedades computadas a su representación subyacente y la consulta será similar a la siguiente consulta

 var employees = (from employee in db.Employees where (employee.FirstName + " " + employee.LastName) == "Test User" select employee).ToList(); 

Si su clase no tiene un atributo [Calculado], puede usar el método de extensión .Computado ().

 var employees = (from employee in db.Employees where employee.FullName.Computed() == "Test User" select employee).ToList(); 

Además, puede llamar a los métodos que devuelven un solo elemento (Cualquiera, Contar, Primero, Único, etc.), así como a otros métodos de la misma manera:

 bool exists = db.Employees.Decompile().Any(employee => employee.FullName == "Test User"); 

Una vez más, la propiedad FullName será descomstackda:

 bool exists = db.Employees.Any(employee => (employee.FirstName + " " + employee.LastName) == "Test User"); 

Soporte asíncrono con EntityFramework

El paquete DelegateDecompiler.EntityFramework proporciona el método de extensión DecompileAsync que agrega soporte para las operaciones asíncronas de EF.


Adicionalmente

Puede encontrar 8 formas de mezclar algunos valores de propiedad en EF aquí:

Propiedades calculadas y marco de la entidad . (escrito por Dave Glick )

Si bien no es una respuesta a su tema, puede ser una respuesta a su problema:

Ya que parece que está dispuesto a modificar la base de datos, ¿por qué no crear una nueva Vista en la base de datos que es básicamente

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

y luego cambiar su modelo de datos para usar TotalledLine en lugar de LineTable?

Después de todo, si su aplicación necesita Total, no es una exageración pensar que otros puedan hacerlo, y la centralización de ese tipo de cálculo en la base de datos es una razón para usar una base de datos.

Solo para agregar algunos de mis propios pensamientos aquí. Como soy tan perfeccionista, no quiero llevar todo el contenido de la tabla a la memoria para poder hacer el cálculo y ordenar y luego la página. Así que de alguna manera la tabla necesita saber cuál es el total.

Lo que estaba pensando era crear un nuevo campo en la tabla de líneas llamada ‘CalcTotal’ que contenga el total calculado de la línea. Este valor se establecería cada vez que se cambie la línea, en función del valor de .Total

Esto tiene 2 ventajas. En primer lugar, puedo cambiar la consulta a esto:

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

La clasificación y la paginación funcionarán porque es un campo db. Y puedo hacer una verificación (line.CalcTotal == line.Total) en mi controlador para detectar cualquier tontería. Causa un poco de sobrecarga en mi repository, es decir, cuando voy a guardar o crear una nueva línea, tendría que agregar

 line.CalcTotal = line.Total 

Pero creo que puede valer la pena.

¿Por qué molestarse en tener 2 propiedades que tienen los mismos datos?

Bueno es cierto, podría poner

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

en mi repository Sin embargo, esto no sería muy ortogonal, y si fuera necesario algún cambio en los totales de línea, tendría mucho más sentido editar la clase de línea parcial que el repository de línea.