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

Force immediate execution of IEnumerable query using Linq2Sql context

我已经阅读了大量关于 "possible multiple enumeration" 问题的帖子。我想我理解延迟执行与立即执行的概念,以及 returning 接口与具体类型的含义。

所以鉴于下面的数据访问层方法和测试代码,我试图强制立即执行查询。 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 来满足客户的需求。相反,客户应妥善处理 returned IEnumerable。所以我的问题是:

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

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

您遇到的问题是由于您处理对象上下文的位置所致。一旦您已经离开 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);        
}

所以,具体回答你的问题:

in this situation, can the customer force the immediate execution (if yes, how)?

不,您不能 "force" 消费者(我认为您所说的 "customer" 是什么意思?)执行查询,除非您自己强制执行查询(通过调用 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();
    }
}

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

should the DAL return .ToList() and/or the signature be modified?

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

但是,如果您可以保证您的数据库上下文在该方法完成执行后保持活动状态(例如,如果您为每个 Web 请求共享一个上下文 class),那么不,您不一定有强制执行。

一种方法是将数据上下文 class 依赖注入到 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);
    }
}

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

如果您不希望 Context 引用从您的 DAL 泄漏,您将必须在 GetItems 方法中执行查询并return 结果。正如您已经通过 .ToList.

所做的那样

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

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

这也将解决您的 "possible multiple enumeration" 问题。