【问题标题】:Using custom method inside Linq Select with Entity Framework在带有实体框架的 Linq Select 中使用自定义方法
【发布时间】:2016-05-18 04:53:01
【问题描述】:

我正在尝试在与 EF 一起使用的 Linq Select 中使用自定义函数。 我想将 tblMitarbeiter 的每一项投影到一个在给定日期有效的 tblMitarbeiterPersonalkostenstelleHistories 上。 这应该使用扩展方法来完成,这样我就不会重复自己;) 我只能在直接在 DbSet 上使用它时才能让它工作,而不是在 Select 内。

我如何教 EF 识别我的方法 (3.),就像我将它写出来 (1.) 一样?

void Main()
{
    var date = DateTime.Now;

    // 1. works, returns IEnumerable<tblMitarbeiterPersonalkostenstelleHistories>
    tblMitarbeiters
    .Select(m => m.tblMitarbeiterPersonalkostenstelleHistories.Where(p => p.ZuordnungGültigAb <= date).OrderByDescending(p => p.ZuordnungGültigAb).FirstOrDefault())
    .Dump();

    // 2. works, returns one tblMitarbeiterPersonalkostenstelleHistories
    tblMitarbeiterPersonalkostenstelleHistories
    .GetValidItemForDate(p => p.ZuordnungGültigAb, date)
    .Dump();

    // 3. throws NotSupportedException
    tblMitarbeiters
    .Select(m => m.tblMitarbeiterPersonalkostenstelleHistories.GetValidItemForDate(p => p.ZuordnungGültigAb, date))
    .Dump();

    // 4. throws NotSupportedException
    tblMitarbeiters
    .Select(m => m.tblMitarbeiterPersonalkostenstelleHistories.AsQueryable().GetValidItemForDate(p => p.ZuordnungGültigAb, date))
    .Dump();
}


public static class QueryableExtensions
{
    public static T GetValidItemForDate<T>(this IQueryable<T> source, Expression<Func<T, DateTime>> selector, DateTime date)
    {
        var dateAccessor = Expression.Lambda<Func<T, DateTime>>(Expression.Constant(date), selector.Parameters);
        var lessThanOrEqual = Expression.LessThanOrEqual(selector.Body, dateAccessor.Body);
        var lambda = Expression.Lambda<Func<T, bool>>(lessThanOrEqual, selector.Parameters);
        return source.Where(lambda).OrderByDescending(selector).FirstOrDefault();
    }

    public static T GetValidItemForDate<T>(this IEnumerable<T> source, Func<T, DateTime> selector, DateTime date) =>
        source.Where(i => selector(i) <= date).OrderByDescending(selector).FirstOrDefault();
}

【问题讨论】:

  • 稳健方式 - 在您的 Select 之前使用自定义方法调用 ToList()。它有效,但我不推荐它。
  • 我想在服务器上,而不是在内存中。 ToList() 要么需要包含(性能不佳),要么需要大量延迟加载(性能更差)
  • 嗯,有 SqlFunctions 可以在 IQueryable 内部调用(在数据库端),但如果你想要更多,你应该使用存储过程或将其加载到内存中。
  • 我的方法已经不能被分解成EF(1.)能识别的简单linq函数,所以不需要SqlFunctions。

标签: c# entity-framework linq extension-methods


【解决方案1】:

在某种程度上,您可以使用LINQKit 拆分复杂的 LINQ 表达式。如果您不介意,我将使用一个不那么日化的示例模型:

public class Employee
{
    public long Id { get; set; }
    public virtual ICollection<EmployeeHistoryRecord> HistoryRecords { get; set; } 
}

public class EmployeeHistoryRecord
{
    public long Id { get; set; }
    public DateTime ValidFrom { get; set; }
    public long EmployeeId { get; set; }
    public Employee Employee { get; set; }
}

如果我正确理解了您的问题,那么它应该与您的问题相同。

在使用 LINQKit 和一般的 LINQ 时,您必须了解,在不使用存储过程的情况下重用查询代码时,您可以使用的唯一工具是拆分和拼接表达式。

您的实用程序方法将转换为以下内容:

private static Expression<Func<IEnumerable<TItem>, TItem>> GetValidItemForDate<TItem>(
            Expression<Func<TItem, DateTime>> dateSelector, 
            DateTime date)
{
    return Linq.Expr((IEnumerable<TItem> items) =>
        items.Where(it => dateSelector.Invoke(it) <= date)
            .OrderByDescending(it => dateSelector.Invoke(it))
            .FirstOrDefault())
        .Expand();
}

此方法的作用是动态创建一个表达式,其输入为IEnumerable&lt;TItem&gt;,返回TITem。您可以看到它与您提取的代码非常相似。需要注意的几点:

  • 源集合不是实用方法的参数,而是返回的表达式的参数。
  • 你必须从 LinqKit 中调用 Invoke() 扩展方法来处理你“插入”这个表达式的任何表达式。
  • 如果你在结果中使用了任何Invoke()s,你应该调用Expand()。这将使 LINQKit 将表达式树中对 Invoke() 的调用替换为被调用的表达式。 (这不是 100% 必要的,但是当由于某种原因扩展失败时,它可以更容易地修复错误。如果您不在每个辅助方法中 Expand(),那么在扩展过程中发生的任何错误都会在执行的方法中表现出来扩展,而不是在实际包含违规代码的方法中。)

然后您可以类似地使用它,再次使用Invoke()

var db = new EmployeeHistoryContext();

var getValidItemForDate = GetValidItemForDate((EmployeeHistoryRecord cab) => cab.ValidFrom, DateTime.Now);

var historyRecords = db.Employees.AsExpandable().Select(emp => getValidItemForDate.Invoke(emp.HistoryRecords));

(我只针对空数据库测试了此代码,只要它不会使 EntityFramework 抛出 NotSupportedException。)

这里,你应该注意:

  • 您插入的子表达式要传递给 Select() 需要保存在局部变量中,LINQKit 不支持扩展期间的方法调用。
  • 您需要在链中的第一个 IQueryable 上调用 AsExpandable(),这样 LINQKit 才能发挥它的魔力。
  • 您可能无法像问题中那样在表达式中使用扩展方法调用语法。
  • 必须在展开之前确定所有子表达式。

这些限制源于您所做的并不是真正调用方法这一事实。您正在从一堆较小的表达式中构建一个巨大的表达式,但生成的表达式本身仍然必须是 LINQ-to-Entities 能够理解的东西。另一方面,输入必须是 LINQKit 能够理解的,并且它只处理 localVariable.Invoke() 形式的表达式。任何动态都必须在此表达式树之外的代码中。基本上,它的作用与您的解决方案 2 相同,只是使用的语法比以编程方式构建表达式树更直观。

最后但并非最不重要的一点是:这样做时,不要过火。当出现任何问题时,复杂的 EF 查询已经很难调试,因为您没有被告知代码中的问题出在哪里。如果查询是从整个代码库中的点点滴滴动态组装的,那么调试一些错误(比如令人愉快的“无法将类型 X 转换为类型 Y”)将很容易成为一场噩梦。


(对于未来的问题:我认为如果您从头开始制作代码示例,而不是使用实际代码库中的位,这通常是一个好主意。它们可能过于特定于域,并且理解名称可能需要一些上下文你认为理所当然。理想情况下,标识符应该是每个人都能理解的简单英文名字。我可能会说足够多的德语来面试一份工作,但是“Mitarbeiterpersonalkostenstellehistorie”很难在我的脑海中记住,并且我什么时候没有实际上在你的项目上工作了足够长的时间来熟悉它的含义。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-18
    • 1970-01-01
    • 1970-01-01
    • 2017-06-15
    相关资源
    最近更新 更多