entity framework中的LINQ预测的可重用计算(代码优先)
我的域模型有很多复杂的财务数据,这是对各种实体的多个属性进行相当复杂计算的结果。 我通常将这些作为[NotMapped]
属性包含在相应的域模型中(我知道,我知道 – 关于将业务逻辑放在您的实体中存在很多争议 – 务实,它只适用于AutoMapper并且允许我定义可重用的DataAnnotations
– a讨论这是好还是不是我的问题。
只要我想实现整个实体(以及任何其他依赖实体,通过.Include()
LINQ调用或在实现后通过其他查询),然后在查询后将这些属性映射到视图模型,这样就可以正常工作。 当尝试通过投影到视图模型而不是实现整个实体来优化有问题的查询时,会出现问题。
考虑以下域模型(显然简化):
public class Customer { public virtual ICollection 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; } }
以下视图模型:
public class CustomerViewModel { public decimal AccountValue { get; set; } }
如果我尝试像这样直接投影:
List customers = MyContext.Customers .Select(x => new CustomerViewModel() { AccountValue = x.AccountValue }) .ToList();
我最终得到以下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.
这是预期的。 我明白了 – entity framework无法将属性getter转换为有效的LINQ表达式。 但是,如果我在投影中使用完全相同的代码进行投影,它可以正常工作:
List customers = MyContext.Customers .Select(x => new CustomerViewModel() { AccountValue = x.Holdings.Sum(y => y.Quantity * y.Stock.Price) }) .ToList();
因此,我们可以得出结论, 实际的逻辑可以转换为SQL查询(即,没有什么特别的东西,如从磁盘读取,访问外部变量等)。
所以这里有一个问题:是否有任何方法可以在LINQ中实现可转换为SQL的逻辑到实体投影中的可重用性?
考虑该计算可以在许多不同的视图模型中使用。 将其复制到每个动作中的投影是麻烦且容易出错的。 如果计算更改为包含乘数怎么办? 我们必须在任何地方手动定位和更改它。
我尝试过的一件事是将逻辑封装在IQueryable
扩展中:
public static IQueryable WithAccountValue( this IQueryable query) { return query.Select(x => new CustomerViewModel() { AccountValue = x.Holdings.Sum(y => y.Quantity * y.Stock.Price) }); }
哪个可以这样使用:
List customers = MyContext.Customers .WithAccountValue() .ToList();
这在一个像这样的简单设计案例中运作良好,但它不可组合。 因为扩展的结果是IQueryable
而不是IQueryable
,所以不能将它们链接在一起。 如果我在一个视图模型中有两个这样的属性,其中一个在另一个视图模型中,然后另一个在第三个视图模型中,我将无法对所有三个视图模型使用相同的扩展 – 这将打败整个目的。 通过这种方法,它是全有或全无。 每个视图模型都必须具有完全相同的计算属性集(很少出现这种情况)。
抱歉这个冗长的问题。 我更愿意提供尽可能详细的信息,以确保人们理解这个问题并可能帮助其他人。 我只是觉得我在这里遗漏了一些会使所有这些成为焦点的东西。
在过去的几天里,我对此进行了大量研究,因为在构建高效的Entity Framework查询时,这是一个痛点。 我发现了几种不同的方法,这些方法基本上归结为相同的基本概念。 关键是采用计算的属性(或方法),将其转换为查询提供程序知道如何转换为SQL的Expression
,然后将其提供给EF查询提供程序。
我发现以下库/代码试图解决这个问题:
LINQ表达式投影
该库允许您直接将可重用逻辑编写为Expression
,然后提供转换以将该Expression
转换为LINQ查询(因为查询不能直接使用Expression
)。 有趣的是,它将被查询提供程序翻译回Expression
。 可重用逻辑的声明如下所示:
private static Expression> projectAverageEffectiveAreaSelector = proj => proj.Subprojects.Where(sp => sp.Area < 1000).Average(sp => sp.Area);
你这样使用它:
var proj1AndAea = ctx.Projects .AsExpressionProjectable() .Where(p => p.ID == 1) .Select(p => new { AEA = Utilities.projectAverageEffectiveAreaSelector.Project() });
注意.AsExpressionProjectable()
扩展来设置投影支持。 然后,在其中一个Expression
定义上使用.Project
扩展名,将Expression
转换为查询。
LINQ翻译
这种方法与LINQ Expression Projection概念非常相似,只是它更灵活一些,并且有几个扩展点。 权衡的是它使用起来也有点复杂。 基本上,您仍然将可重用逻辑定义为Expression
,然后依赖库将其转换为查询可以使用的内容。 有关详细信息,请参阅博客文章。
DelegateDecompiler
我通过Jimmy Bogard博客上的博客文章找到了DelegateDecompiler。 它一直是救星。 它运作良好,架构良好,需要的仪式少得多。 它不要求您将可重用计算定义为Expression
。 相反,它通过使用Mono.Reflection
来Mono.Reflection
地反编译代码来构造必要的Expression
。 它知道需要通过使用ComputedAttribute
修饰它们或在查询中使用.Computed()
扩展来反编译哪些属性,方法等:
class Employee { [Computed] public string FullName { get { return FirstName + " " + LastName; } } public string LastName { get; set; } public string FirstName { get; set; } }
这也可以很容易地扩展,这是一个很好的接触。 例如,我将其设置为查找NotMapped
数据注释,而不必显式使用ComputedAttribute
。
一旦设置了实体,就可以使用.Decompile()
扩展来触发反编译:
var employees = ctx.Employees .Select(x => new { FullName = x.FullName }) .Decompile() .ToList();
您可以通过创建包含原始实体和其他计算属性的类来封装逻辑。 然后创建投射到类的辅助方法。
例如,如果我们尝试计算Employee
和Contractor
实体的税,我们可以这样做:
//This is our container for our original entity and the calculated field public class PersonAndTax { public T Entity { get; set; } public double Tax { get; set; } }
public class PersonAndTaxHelper { // This is our middle translation class // Each Entity will use a different way to calculate income private class PersonAndIncome { public T Entity { get; set; } public int Income { get; set; } }
收入计算方法
public static IQueryable> GetEmployeeAndTax(IQueryable employees) { var query = from x in employees select new PersonAndIncome { Entity = x, Income = x.YearlySalary }; return CalcualateTax(query); } public static IQueryable> GetContratorAndTax(IQueryable contractors) { var query = from x in contractors select new PersonAndIncome { Entity = x, Income = x.Contracts.Sum(y => y.Total) }; return CalcualateTax(query); }
税收计算在一个地方定义
private static IQueryable> CalcualateTax(IQueryable> personAndIncomeQuery) { var query = from x in personAndIncomeQuery select new PersonAndTax { Entity = x.Entity, Tax = x.Income * 0.3 }; return query; } }
我们的视图模型使用Tax属性进行预测
var contractorViewModel = from x in PersonAndTaxHelper.GetContratorAndTax(context.Contractors) select new { x.Entity.Name, x.Entity.BusinessName x.Tax, }; var employeeViewModel = from x in PersonAndTaxHelper.GetEmployeeAndTax(context.Employees) select new { x.Entity.Name, x.Entity.YearsOfService x.Tax, };