My domain model has a lot of complex financial data, which is the result of fairly complex calculations on several properties of various objects. I usually include them as properties of [NotMapped] in a suitable domain model (I know, I know there is a lot of controversy about placing business logic in your entities), being pragmatic, it just works well with AutoMapper and allows me to define reusable DataAnnotations - discussion whether this is good or not is not my question).
This works fine as long as I want to materialize the entire object (and any other dependent objects either through .Include() LINQ calls, or through additional queries after materialization), and then map these properties to the view model after the query. The problem arises when trying to optimize problematic queries by projecting into the presentation model instead of materializing the entire object.
Consider the following domain models (obviously simplified):
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; } }
And the following presentation model:
public class CustomerViewModel { public decimal AccountValue { get; set; } }
If I try to do it right this way:
List<CustomerViewModel> customers = MyContext.Customers .Select(x => new CustomerViewModel() { AccountValue = x.AccountValue }) .ToList();
The result is the following NotSupportedException : Additional information: The specified type member 'AccountValue' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported. Additional information: The specified type member 'AccountValue' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.
Expected. I get this: Entity Framework cannot convert getters properties to a valid LINQ expression. However, if I project the exact code, but within the projection, it works fine:
List<CustomerViewModel> customers = MyContext.Customers .Select(x => new CustomerViewModel() { AccountValue = x.Holdings.Sum(y => y.Quantity * y.Stock.Price) }) .ToList();
So, we can conclude that the actual logic is converted to an SQL query (for example, there is nothing exotic like reading from disk, accessing external variables, etc.).
So here's the question: is there any way to make logic that should be convertible to SQL reusable in LINQ for entities?
Note that this calculation can be used in many different look models. Copying it onto a projection in each action is cumbersome and error prone. What if the multiplier is included in the calculation? We must manually find and modify it wherever it was used.
One thing I tried is encapsulating logic in an IQueryable extension:
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) }); }
What can be used as follows:
List<CustomerViewModel> customers = MyContext.Customers .WithAccountValue() .ToList();
This works well in a simple far-fetched case, but it is not complicated. Since the result of the extension is IQueryable<CustomerViewModel> and not a IQueryable<Customer> , you cannot bind them together. If I had two of these properties in one presentation model, one in the other presentation model, and then the other in the third presentation model, I would have no way to use the same extension for all three presentation models - to defeat whole goal. With this approach, all this or nothing. Each presentation model must have exactly the same set of calculated properties (which is rarely the case).
Sorry for the long question. I prefer to provide as much detail as possible so that people can understand the issue and potentially help others in the future. I just feel like I'm missing something that would make all of this in focus.