在 LINQ to Entities 中重用选择表达式

Posted

技术标签:

【中文标题】在 LINQ to Entities 中重用选择表达式【英文标题】:Reuse select expressions in LINQ to Entities 【发布时间】:2018-02-20 01:54:39 【问题描述】:

我有一些复杂的业务定义,我想定义一次并在 Linq to Entities 中重用它们,并在基于它的其他表达式中使用它们。

我的尝试如下,这在我最初传递ConvertOrderDetailsToViewModelList 时有效,但我想在IQueryable 上执行此操作,这会导致错误:

LINQ to Entities does not recognize the method 'System.Decimal Invoke(OrderDetail)' method, and this method cannot be translated into a store expression.

有没有办法在本地实现这一点(没有 3rd 方库)? 从this answer 看来,您可以在数据库本身中定义这些函数,然后从 C# 调用它们,但同样,如果可能的话,希望仅在 C# 代码中执行此操作。

还遇到了this answer,它看起来适用于Where 表达式,但是当我尝试为我的select 表达式实现它时,我得到了这个错误,因为我正在尝试将表达式分配给小数。

Cannot implicitly convert type Expression<Func<OrderDetail, decimal>> to 'decimal'

这是表达式函数:

public static Expression<Func<OrderDetail, decimal>> calcQtyNeedOD = (od) => (od.OrderedQuantity - od.PickedQuantity); 

调用该函数的视图模型创建方法:

public List<AllocationNeedViewModel> ConvertOrderDetailsToViewModel(IQueryable<OrderDetail> qryShipOrderDtls, List<int> itemIdsToExclude, bool boolGroupViewModelByItemAndRemnant)

    var qryAllocNeedVMTEST = 
        from od in qryShipOrderDtls
        select new AllocationNeedViewModel()
        
            WorkReleaseHeaderId = od.OrderHeader.WorkReleaseHeaderId.Value,
            OrderHeaderId = od.OrderHeaderId,
            OrderDetailId = od.Id,
            ItemId = od.ItemId.Value,
            ItemDescription = od.Item.Description,
            IsInventoryTracked = od.Item.TrackInventory,
            QtyNeed = calcQtyNeedOD(od),
            QtyRemain = 0,
            QtyAllocated = 0,
            IsAllocated = false
        
        ;
        return qryAllocNeedVMTEST.ToList();

为了使这更复杂,还有其他属性我也希望有一个可重用的表达式,它也将使用第一个表达式...即

public static readonly Expression<Func<OrderDetail, decimal>> calcQtyRemainOD =
    (od) => calcQtyNeedOD.Compile().Invoke(od) - calcQtyAllocatedOD.Compile().Invoke(od); 

更新 #1

查看更新 #2...此解决方案不起作用!

虽然到目前为止还没有人能够提供一种本地方式来重用选择表达式查询,但我确实找到了一个部分解决方案,可以在重用它们em> 相同的查询。一旦将表达式投影/分配给实体的属性,您就可以(与 T-SQL 不同)将该属性指定为另一个后续属性表达式的一部分。

示例 - 这显示了每个属性投影的完整表达式。在此示例中,QtyRemain 本质上只是QtyNeed - QtyAllocated。我在QtyRemain 分配中再次指定了这些:

public List<AllocationNeedViewModel> ConvertOrderDetailsToViewModel(IQueryable<OrderDetail> qryShipOrderDtls, List<int> itemIdsToExclude, bool boolGroupViewModelByItemAndRemnant)

    var qryAllocNeedVM = qryShipOrderDtls
        .Select(od => new AllocationNeedViewModel() //Get all Work Order Detail Needs for Work Release
        
            QtyNeed = (od.OrderedQuantity - od.PickedQuantity), 
            QtyAllocated = (od.AllocatedInventories.Count == 0 ? 0 : od.AllocatedInventories.Where(ai => ai.StatusId < _AllocatedInventoryProcessedStatus).Sum(ai => ai.AllocatedQty)), 
            QtyRemain = (od.OrderedQuantity - od.PickedQuantity) - (od.AllocatedInventories.Count == 0 ? 0 : od.AllocatedInventories.Where(ai => ai.StatusId < _AllocatedInventoryProcessedStatus).Sum(ai => ai.AllocatedQty))
        
        );
        return qryAllocNeedVM.ToList();

相反,您可以简单地使用 QtyRemain 属性分配中已定义的属性,如下所示:

public List<AllocationNeedViewModel> ConvertOrderDetailsToViewModel(IQueryable<OrderDetail> qryShipOrderDtls, List<int> itemIdsToExclude, bool boolGroupViewModelByItemAndRemnant)

    var qryAllocNeedVM = qryShipOrderDtls
        .Select(od => new AllocationNeedViewModel() //Get all Work Order Detail Needs for Work Release
        
            QtyNeed = (od.OrderedQuantity - od.PickedQuantity), 
            QtyAllocated = (od.AllocatedInventories.Count == 0 ? 0 : od.AllocatedInventories.Where(ai => ai.StatusId < _AllocatedInventoryProcessedStatus).Sum(ai => ai.AllocatedQty)), 
            QtyRemain = this.QtyNeed - this.QtyAllocated
        
        );
        return qryAllocNeedVM.ToList();

虽然这不是我最初问题的完整解决方案,但它是一个部分解决方案,可以为您带来一些所需的好处。

更新 #2

我在更新 #1 上错了。虽然这可以工作和编译并且似乎生成 SQL,但它是不正确的。 this.QtyNeed 的返回值在后面的表达式中使用时总是返回0。 :(

【问题讨论】:

看看LINQKit 感谢@IvanStoev,但如果可能的话,现在尽量不要使用第三方库。似乎很多这些最终在短时间内被放弃,然后当您将项目升级到下一个 .net 框架时不起作用,然后您不得不修改一堆代码。 @ChadRichardson 如果您愿意,您可以编写代码自己做,但您需要做或多或少与该代码库相同的事情。你当然也可以找到其他人的实现。 @Servy,明白了。我只是希望在 2017 年有一种本地方式来做到这一点(事情每年都在变化)。如果答案是否定的……那我肯定会看看第三方库路线……或者创建一个数据库视图。 另一种选择是使用 UDF 并从您的查询中调用它。这至少可以避免将所有内容都写入视图。 【参考方案1】:

您是否考虑过以下几点?:

public static Func<OrderDetail, decimal> calcQtyNeedOD = (od) => (od.OrderedQuantity - od.PickedQuantity);

public List<AllocationNeedViewModel> ConvertOrderDetailsToViewModel(IQueryable<OrderDetail> qryShipOrderDtls, List<int> itemIdsToExclude, bool boolGroupViewModelByItemAndRemnant)

    return qryShipOrderDtls
        .ToArray()
        .Select(new AllocationNeedViewModel
            
                WorkReleaseHeaderId = od.OrderHeader.WorkReleaseHeaderId.Value,
                OrderHeaderId = od.OrderHeaderId,
                OrderDetailId = od.Id,
                ItemId = od.ItemId.Value,
                ItemDescription = od.Item.Description,
                IsInventoryTracked = od.Item.TrackInventory,
                QtyNeed = calcQtyNeedOD(od),
                QtyRemain = 0,
                QtyAllocated = 0,
                IsAllocated = false
            ).ToList();

原始查询试图针对实际数据库执行 Func(它不会理解您的表达式)。由于您没有过滤查询(无 where 子句),请返回整个集合,以便您可以使用本地 Func 进行投影。

【讨论】:

该问题明确指出他们正在这样做,但它(可以理解)变得太成问题,无法始终在内存中完成所有这些工作,并且需要在数据库端完成。总是将你所做的每一个查询都具体化到内存中是站不住脚的。 原始问题没有提到内存要求/问题。只是他们希望将业务逻辑保留在 C# 中。 是的,确实如此,“我最初是向 ConvertOrderDetailsToViewModel 传递一个列表,但我想在 IQueryable 上执行此操作,” Correct @Servy,Eric 我按照你说的做,但原来的 qryShipOrderDtls 从多个表中带回了大量列,所以在 IQueryable 中尝试这样做的全部目的是减少数据只需拉回这几列即可加载和记忆。

以上是关于在 LINQ to Entities 中重用选择表达式的主要内容,如果未能解决你的问题,请参考以下文章

在Linq To Entities中使用多个列联接表

使用Linq to Entities在一个请求中选择计数和计数

LINQ to Entities 选择新建

Linq to Entities 添加 Where 子句以在另一个表中查找 EXISTS

查询 LINQ to Entities 中联系字段的属性

使用 Linq to Entities (EF6) 动态选择列名