使用Linq2Sql上下文强制立即执行IEnumerable查询

本文关键字:执行 IEnumerable 查询 Linq2Sql 上下文 使用 | 更新日期: 2023-09-27 18:29:44

我已经阅读了大量关于"可能的多重枚举"问题的文章。我想我理解延迟执行与立即执行的概念,以及返回接口与具体类型的含义。

因此,给定下面的数据访问层方法和测试代码,我试图强制立即执行查询。ToList()在数据访问层方法中工作,但在Main方法中不工作(可能是因为ToList()是在处理上下文后调用的)。铸造as ReadOnlyCollection<Item>(或IReadOnlyCollection)也不起作用。

    static void Main(string[] args)
    {
        var foo = GetItems(i => i.SupiCode.Contains("TestCode")).ToList(); // ObjectDisposedException (context)
    }
    private static IEnumerable<Item> GetItems(Func<Item, bool> filter)
    {
        using (var ctx = new RRPClassesDataContext())
        {
            return ctx.Item.Where(filter); //.ToList(); <-- this works
        }
    }

我的目标是防止多次枚举(即多次访问数据库)。从我读到的内容来看,我不应该修改DAL来满足客户的需求。相反,客户应该正确处理返回的IEnumerable。所以我的问题是:

  • 在这种情况下,客户是否可以强制立即执行(如果可以,如何执行)
  • DAL是否应返回.ToList()和/或修改签名

使用Linq2Sql上下文强制立即执行IEnumerable查询

您看到的问题是由于处理对象上下文的位置造成的。一旦您已经离开了GetItems方法,就要调用ToList,该方法将处理它试图执行的对象上下文。因此,查询是针对已处理的上下文执行的,您会得到异常。

您可以通过稍微更改代码来验证这一点,如下所示:

static void Main(string[] args)
{
    using (var ctx = new RRPClassesDataContext())
    {
        var foo = GetItems(ctx, i => i.SupiCode.Contains("TestCode"));
        // force execution. context is still open so query works.
        var bar = foo.ToList();
    }
}
private static IEnumerable<Item> GetItems(RRPClassesDataContext ctx, Func<Item, bool> filter)
{
    return ctx.Item.Where(filter);        
}

因此,具体回答您的问题:

在这种情况下,客户是否可以强制立即执行(如果可以,如何执行)?

不,您不能"强制"消费者(我认为这就是您所说的"客户"?)执行查询,除非您自己强制执行查询(通过调用ToListToArray等)。请注意,您可以在不违反API:的情况下执行此操作

private static IEnumerable<Item> GetItems(Func<Item, bool> filter)
{
    using (var ctx = new RRPClassesDataContext())
    {
        // Forces execution and safely allows the context to be disposed.
        // Still returns an IEnumerable<Item> so the method contract
        // is preserved.
        return ctx.Item.Where(filter).ToList();
    }
}

这是确保查询不会被多次执行的唯一方法。

DAL是否应返回.ToList()和/或签名进行修改?

这取决于应用程序的体系结构。如果您的DAL将负责打开和关闭数据库连接(即创建/销毁上下文类),则您别无选择-您必须在上下文关闭之前强制执行(以避免上下文处理的异常)。

但是,如果你可以保证你的DB上下文在方法完成执行后保持活动(例如,如果你每个web请求共享一个上下文类),那么就不必强制执行。

这样做的一种方法是将数据上下文类依赖性注入DAL,类似于:

public class ItemDAL
{
    private readonly RRPClassesDataContext _dataContext;
    public ItemDAL(RRPClassesDataContext dataContext)
    {
        _dataContext = dataContext;
    }
    public IQueryable<Item> GetItems(Func<Item, bool> filter)
    {
        return _dataContext.Items.Where(filter);
    }
}

这种方法的缺点是(正如您所暗示的)您不能保证最终不会执行查询两次(因为现在由调用方强制执行查询)。

编写这个ctx.Item.Where(filter)linq只会创建一个linq-to-sql查询,它只会在调用ToList()时执行。如果您通过List进行枚举,那么查询将针对DB运行,因为您将以IEnumerable的形式返回到main类,而ToList()将强制执行查询并抛出错误。

是的,您应该通过ToList()强制立即执行。

如果不希望Context引用从DAL中泄漏,则必须在GetItems方法内部执行查询并返回结果。正如你已经通过.ToList.做的那样

在我看来,在你的情况下,这也是写作的事情。您希望查询立即执行并返回结果,创建上下文实例是一项非常廉价的操作,因此在执行查询时创建它是一种很好的做法。GetItem方法签名可以更改为

private static ICollection<Item> GetItems(Func<Item, bool> filter)
    {
        using (var ctx = new RRPClassesDataContext())
        {
            return ctx.Item.Where(filter).ToList(); <-- this works
        }
    }

这也将解决您的"可能的多重枚举"问题。