Cálculos reutilizáveis para projeções LINQ no Entity Framework (código primeiro)

Meu modelo de domínio possui muitos dados financeiros complexos, resultado de cálculos bastante complexos em várias propriedades de várias entidades. Eu geralmente os incluo como[NotMapped] propriedades no modelo de domínio apropriado (eu sei, eu sei - há muito debate em torno de colocar a lógica de negócios em suas entidades - sendo pragmático, ele funciona bem com o AutoMapper e me permite definir reutilizávelDataAnnotations - uma discussão sobre se isso é bom ou não, não é minha pergunta).

Isso funciona bem desde que eu queira materializar toda a entidade (e quaisquer outras entidades dependentes, via.Include() Chamadas LINQ ou por meio de consultas adicionais após a materialização) e, em seguida, mapeie essas propriedades para o modelo de exibição após a consulta. O problema surge ao tentar otimizar consultas problemáticas projetando um modelo de exibição em vez de materializar toda a entidade.

Considere os seguintes modelos de domínio (obviamente simplificados):

public class Customer
{
 public virtual ICollection<Holding> Holdings { get; private set; }

 [NotMapped]
 public decimal AccountValue
 {
  get { return Holdings.Sum(x => x.Value); }
 }
}

public class Holding
{
 public virtual Stock Stock { get; set; }
 public int Quantity { get; set; }

 [NotMapped]
 public decimal Value
 {
  get { return Quantity * Stock.Price; }
 }
}

public class Stock
{
 public string Symbol { get; set; }
 public decimal Price { get; set; }
}

E o seguinte modelo de vista:

public class CustomerViewModel
{
 public decimal AccountValue { get; set; }
}

Se eu tentar projetar diretamente assim:

List<CustomerViewModel> customers = MyContext.Customers
 .Select(x => new CustomerViewModel()
 {
  AccountValue = x.AccountValue
 })
 .ToList();

Eu termino com o seguinteNotSupportedException: Additional information: The specified type member 'AccountValue' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.

O que é esperado. Entendi - o Entity Framework não pode converter os getters de propriedade em uma expressão LINQ válida. No entanto, se eu projetarusando exatamente o mesmo código mas dentro da projeção, funciona bem:

List<CustomerViewModel> customers = MyContext.Customers
 .Select(x => new CustomerViewModel()
 {
  AccountValue = x.Holdings.Sum(y => y.Quantity * y.Stock.Price)
 })
 .ToList();

Então, podemos concluir que ológica real é conversível em uma consulta SQL (ou seja, não há nada exótico como ler do disco, acessar variáveis externas etc.).

Então aqui está a pergunta: existe alguma maneira de fazer lógica quedevemos pode ser convertido em SQL reutilizável no LINQ para projeções de entidade?

Considere que esse cálculo pode ser usado em muitos modelos de vista diferentes. Copiá-lo para a projeção em cada ação é complicado e propenso a erros. E se o cálculo mudar para incluir um multiplicador? Teríamos que localizá-lo e alterá-lo manualmente em todos os lugares em que for usado.

Uma coisa que tentei é encapsular a lógica dentro de umIQueryable extensão:

public static IQueryable<CustomerViewModel> WithAccountValue(
 this IQueryable<Customer> query)
{
 return query.Select(x => new CustomerViewModel()
 {
  AccountValue = x.Holdings.Sum(y => y.Quantity * y.Stock.Price)
 });
}

Que pode ser usado assim:

List<CustomerViewModel> customers = MyContext.Customers
 .WithAccountValue()
 .ToList();

Isso funciona bem o suficiente em um caso simples como esse, mas não é compostável. Como o resultado da extensão é umIQueryable<CustomerViewModel> e não umIQueryable<Customer> você não pode amarrá-los juntos. Se eu tivesse duas dessas propriedades em um modelo de vista, uma delas em outro modelo de vista e a outra em um modelo de terceira vista, não teria como usar a mesma extensão para todos os três modelos de vista - o que derrotaria o conjunto objetivo. Com essa abordagem, é tudo ou nada. Todo modelo de vista precisa ter exatamente o mesmo conjunto de propriedades calculadas (o que raramente é o caso).

Desculpe pela pergunta prolongada. Prefiro fornecer o máximo de detalhes possível para garantir que as pessoas entendam a pergunta e potencialmente ajude outras pessoas no caminho. Eu sinto que estou perdendo algo aqui que colocaria tudo isso em foco.

questionAnswers(2)

yourAnswerToTheQuestion