【问题标题】:Reusable Calculations For LINQ Projections In Entity Framework (Code First)实体框架中 LINQ 投影的可重用计算(代码优先)
【发布时间】:2014-12-05 21:02:39
【问题描述】:

我的域模型有很多复杂的财务数据,这些数据是对各种实体的多个属性进行相当复杂计算的结果。我通常将这些作为[NotMapped] 属性包含在适当的域模型上(我知道,我知道 - 关于将业务逻辑放入您的实体中有很多争论 - 务实,它与 AutoMapper 配合得很好,让我定义可重用的 DataAnnotations - 讨论这是否好不是我的问题)。

只要我想具体化整个实体(以及任何其他依赖实体,通过.Include() LINQ 调用或通过具体化后的其他查询),然后在查询后将这些属性映射到视图模型,这工作正常。当尝试通过投影到视图模型而不是物化整个实体来优化有问题的查询时,就会出现问题。

考虑以下领域模型(显然是简化的):

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

以及以下视图模型:

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

如果我尝试像这样直接投影:

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

我最终得到以下NotSupportedExceptionAdditional information: The specified type member 'AccountValue' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.

这是预期的。我明白了 - 实体框架无法将属性获取器转换为有效的 LINQ 表达式。但是,如果我使用完全相同的代码进行投影,但在投影中,它可以正常工作:

List<CustomerViewModel> 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<CustomerViewModel> WithAccountValue(
 this IQueryable<Customer> query)
{
 return query.Select(x => new CustomerViewModel()
 {
  AccountValue = x.Holdings.Sum(y => y.Quantity * y.Stock.Price)
 });
}

可以这样使用:

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

在这样一个简单的人为情况下,这已经足够了,但它不是可组合的。因为扩展的结果是IQueryable&lt;CustomerViewModel&gt; 而不是IQueryable&lt;Customer&gt;,所以不能将它们链接在一起。如果我在一个视图模型中有两个这样的属性,一个在另一个视图模型中,然后另一个在第三个视图模型中,我将无法对所有三个视图模型使用相同的扩展 - 这会破坏整个目的。使用这种方法,要么全有,要么全无。每个视图模型都必须具有完全相同的一组计算属性(这种情况很少见)。

很抱歉这个冗长的问题。我更愿意提供尽可能多的细节,以确保人们理解问题并可能帮助其他人。我只是觉得我在这里遗漏了一些可以让所有这些都成为焦点的东西。

【问题讨论】:

    标签: c# linq entity-framework linq-to-entities


    【解决方案1】:

    过去几天我对此进行了大量研究,因为在构建高效的实体框架查询方面有点痛点。我发现了几种不同的方法,它们基本上都归结为相同的基本概念。关键是获取计算的属性(或方法),将其转换为查询提供程序知道如何转换为 SQL 的Expression,然后将其提供给 EF 查询提供程序。

    我找到了以下试图解决此问题的库/代码:

    LINQ 表达式投影

    http://www.codeproject.com/Articles/402594/Black-Art-LINQ-expressions-reusehttp://linqexprprojection.codeplex.com/

    此库允许您将可重用逻辑直接编写为Expression,然后提供转换以将该Expression 转换为您的LINQ 查询(因为查询不能直接使用Expression)。有趣的是它会被查询提供者翻译回Expression。可重用逻辑的声明如下所示:

    private static Expression<Func<Project, double>> 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<double>() 
      });
    

    注意.AsExpressionProjectable() 扩展以设置投影支持。然后,您在 Expression 定义之一上使用 .Project&lt;T&gt;() 扩展名,将 Expression 放入查询中。

    LINQ 翻译

    http://damieng.com/blog/2009/06/24/client-side-properties-and-any-remote-linq-providerhttps://github.com/damieng/Linq.Translations

    这种方法与 LINQ 表达式投影概念非常相似,只是它更灵活一些,并且有几个扩展点。权衡是它的使用也有点复杂。本质上,您仍然将可重用逻辑定义为Expression,然后依靠库将其转换为查询可以使用的内容。有关详细信息,请参阅博文。

    委托反编译器

    http://lostechies.com/jimmybogard/2014/05/07/projecting-computed-properties-with-linq-and-automapper/https://github.com/hazzik/DelegateDecompiler

    我通过 Jimmy Bogard 博客上的博文找到了 DelegateDecompiler。它一直是救命稻草。它运作良好,架构良好,并且需要的仪式要少得多。它不需要您将可重用计算定义为Expression。相反,它通过使用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();
    

    【讨论】:

    • 我看不出这是如何在不同的Entity 类型中重用的。您在所有示例中都有强有力的参考。
    • 与其说可以跨实体类型重用,不如说在单个投影中具有多个计算属性。例如,在上面的Employee 类中,假设我们还有一个Initials 属性是LastName[0] + FirstName[0]。如果我想只用FullName 做一个投影,然后用 both FullNameInitials 做一个投影,我需要每个投影的包装器或链接特定于属性的包装器使用你的方法。在简单的情况下这很好,当它是你所需要的,但它不是一概而论的。最初的问题要求可组合的解决方案。
    • 您可以将所有计算的属性放在一个投影类 (EntityAndCalculatedProperties) 中,然后在视图模型投影中只选择所需的属性。发送到上下文提供者的最终语句将发出任何未使用的计算。
    【解决方案2】:

    您可以通过创建一个包含原始实体和附加计算属性的类来封装逻辑。然后,您创建投影到该类的辅助方法。

    例如,如果我们尝试计算 EmployeeContractor 实体的税,我们可以这样做:


    //This is our container for our original entity and the calculated field
    public class PersonAndTax<T> 
    {
        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<T>
        {
            public T Entity { get; set; }
            public int Income { get; set; }
        }
    

    收入计算方法

        public static IQueryable<PersonAndTax<Employee>> GetEmployeeAndTax(IQueryable<Employee> employees)
        {
            var query = from x in employees
                        select new PersonAndIncome<Employee>
                        {
                            Entity = x,
                            Income = x.YearlySalary
                        };
            return CalcualateTax(query);
        }
    
        public static IQueryable<PersonAndTax<Contractor>> GetContratorAndTax(IQueryable<Contractor> contractors)
        {
            var query = from x in contractors
                        select new PersonAndIncome<Contractor>
                        {
                            Entity = x,
                            Income = x.Contracts.Sum(y => y.Total) 
                        };
    
            return CalcualateTax(query);
        }
    

    税收计算在一处定义

        private static IQueryable<PersonAndTax<T>> CalcualateTax<T>(IQueryable<PersonAndIncome<T>> personAndIncomeQuery)
        {
            var query = from x in personAndIncomeQuery
                        select new PersonAndTax<T>
                        {
                            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,
                                };
    

    【讨论】:

    • 如果您只需要为每个投影使用一个计算属性,这种方法就足够了。但是,如果您需要在每个投影中组合多个计算属性,它就会崩溃。在这种情况下,您最终会得到一堆链接的包装器,所有这些包装器都必须解开到最终的投影对象中。这也让它变得非常脆弱,因为在链的末尾添加额外的计算意味着需要调整所有最终属性以考虑额外的包装器。你最终会得到这样的任务:x.Wrapper.Wrapper.Wrapper.Entity.Name
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-09
    • 2017-09-01
    相关资源
    最近更新 更多