在Linq中使用自定义方法选择实体框架

时间:2016-02-08 13:42:56

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

我正在尝试在与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();
}

1 个答案:

答案 0 :(得分: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<TItem>,返回TITem。您可以看到它与您正在提取的代码非常相似。有几点需要注意:

  • 源集合不是实用程序方法的参数,而是返回表达式的参数。
  • 你必须在LinqKit上调用Invoke()扩展方法来解决你已经插入&#34;插入&#34;这个。
  • 如果您在其中使用了Expand(),则应该在结果上调用Invoke()。这将使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在扩展期间不支持方法调用。
  • 你需要在链中的第一个AsExpandable()上调用IQueryable,这样LINQKit才能发挥其魔力。
  • 您可能无法像在您的问题中那样在表达式中使用扩展方法调用语法。
  • 所有子表达式必须在扩展发生之前确定。

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

最后但同样重要的是:在这样做的时候,不要过火。当出现任何问题时,复杂的EF查询已经很难调试,因为您没有告诉代码在哪里出现问题。如果查询是通过代码库中的零碎动态组装的,那么调试一些错误(比如令人愉快的&#34;无法将类型X转换为类型Y&#34;)将很容易成为一场噩梦。

(对于未来的问题:如果您从头开始编写代码示例,而不是使用实际代码库中的位,我认为通常是个好主意。它们可能过于特定于域,并且理解名称可能需要一些你理所当然的背景。标识符理想上应该是每个人都能理解的简单的英文名称。我可以说足够的德语来面试其中的工作,但是&#34; Mitarbeiterpersonalkostenstellehistorie&#34;很难留在我的关于什么时候我实际上没有在你的项目上工作足够长的时间来熟悉它应该是什么意思的头脑和原因。)