我可以克隆一个IQueryable以便在另一个DbContext的DbSet上运行吗?

时间:2018-06-28 04:38:09

标签: c# async-await entity-framework-6 dbcontext

假设我已经通过许多步骤通过一些条件逻辑构建了一个IQueryable<T>实例,我们将其称为query

我想获取总记录数和一页数据,所以我想调用query.CountAsync()query.Skip(0).Take(10).ToListAsync()。我无法连续调用它们,因为发生竞争情况时,它们都试图在同一DbContext上同时运行查询。这是不允许的:

  

“在先前的异步操作完成之前,第二个操作在此上下文上开始。使用'await'确保在此上下文上调用另一个方法之前所有异步操作都已完成。不能保证任何实例成员都是线程安全的。”

我不想先等待第一个,甚至开始第二个。我想尽快取消这两个查询。唯一的方法是从单独的DbContext运行它们。我可能不得不从DbSet的不同实例开始并排构建整个查询(或2或3),这似乎很荒谬。是否有任何方法可以克隆或更改IQueryable<T>(不一定是该接口,但它是基础实现),这样我可以有一个副本在DbContext“ A”上运行,而另一个副本在DbContext“ B”,以便两个查询可以同时执行?我只是想避免从头开始重新组织X次查询,只是为了在X上下文上运行它。

3 个答案:

答案 0 :(得分:2)

您可以编写一个函数,以DbContext作为参数来构建查询。

 public IQueryable<T> MyQuery(DbContext<T> db)
 {
     return db.Table
              .Where(p => p.reallycomplex)
              ....
              ...
              .OrderBy(p => p.manythings);
 }

我已经做了很多次,并且效果很好。
现在,可以轻松地在两个不同的上下文中进行查询:

IQueryable<T> q1 = MyQuery(dbContext1);
IQueryable<T> q2 = MyQuery(dbContext2);

如果您关心的是构建IQueryable对象所需的执行时间,那么我唯一的建议就是不用担心。

答案 1 :(得分:2)

没有标准的方法可以做到这一点。问题在于EF6查询表达式树包含包含ObjectQuery实例的常量节点,这些实例绑定到创建查询时使用的DbContext(实际上是底层ObjectContext)。另外,在执行查询之前还要进行一次运行时检查,检查是否存在与执行查询的上下文绑定的表达式。

我想到的唯一想法是用ExpressionVisitor处理查询表达式树,并用绑定到新上下文的新实例替换这些ObjectQuery实例。

以下是上述想法的可能实现方式:

using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;

namespace System.Data.Entity
{
    public static class DbQueryExtensions
    {
        public static IQueryable<T> BindTo<T>(this IQueryable<T> source, DbContext target)
        {
            var binder = new DbContextBinder(target);
            var expression = binder.Visit(source.Expression);
            var provider = binder.TargetProvider;
            return provider != null ? provider.CreateQuery<T>(expression) : source;
        }

        class DbContextBinder : ExpressionVisitor
        {
            ObjectContext targetObjectContext;
            public IQueryProvider TargetProvider { get; private set; }
            public DbContextBinder(DbContext target)
            {
                targetObjectContext = ((IObjectContextAdapter)target).ObjectContext;
            }
            protected override Expression VisitConstant(ConstantExpression node)
            {
                if (node.Value is ObjectQuery objectQuery && objectQuery.Context != targetObjectContext)
                    return Expression.Constant(CreateObjectQuery((dynamic)objectQuery));
                return base.VisitConstant(node);
            }
            ObjectQuery<T> CreateObjectQuery<T>(ObjectQuery<T> source)
            {
                var parameters = source.Parameters
                    .Select(p => new ObjectParameter(p.Name, p.ParameterType) { Value = p.Value })
                    .ToArray();
                var query = targetObjectContext.CreateQuery<T>(source.CommandText, parameters);
                query.MergeOption = source.MergeOption;
                query.Streaming = source.Streaming;
                query.EnablePlanCaching = source.EnablePlanCaching;
                if (TargetProvider == null)
                    TargetProvider = ((IQueryable)query).Provider;
                return query;
            }
        }
    }
}

与标准EF6 LINQ查询的一个区别是,它生成ObjectQuery<T>而不是DbQuery<T>,尽管除了ToString()不返回生成的SQL之外,我没有注意到任何区别在进一步的查询构建/执行中。它似乎可行,但使用时要谨慎并自担风险。

答案 2 :(得分:0)

因此,您有一个IQueryable<T>,它将在查询执行后立即在DbContext A上执行,并且您希望在查询执行时在DbContext B上运行相同的查询。

为此,您必须了解IEnumerable<T>IQueryable<T>之间的区别。

IEnumerable<T>包含所有代码以枚举可枚举表示的元素。枚举在调用GetEnumeratorMoveNext时开始。可以明确地做到这一点。但是,通常是通过foreachToListFirstOrDefault等函数隐式完成的。

IQueryable不包含要枚举的代码,它包含一个Expression和一个Provider。提供者知道谁将执行查询,并且知道如何将Expression转换为查询执行者可以理解的语言。

由于这种分离,有可能让相同的Expression由不同的数据源执行。它们甚至不必是同一类型:一个数据源可以是一个能够理解SQL的数据库管理系统,另一个数据源可以是一个逗号分隔的文件。

只要连接返回IQueryable的Linq语句,就不会执行查询,只会更改表达式。

在枚举开始时(通过调用GetEnumerator / MoveNext或使用foreach或不返回IQueryable的LINQ函数之一),提供程序会将表达式转换为数据源可以理解并与之通信的语言数据源以执行查询。查询的结果是IEnumerable,可以将其枚举为好像所有数据都在本地代码中一样。

某些提供程序很聪明,并且使用一些缓冲,因此并非所有数据都传输到本地内存,而是仅传输部分数据。需要时查询新数据。因此,如果在具有成千上万个元素的数据库中进行前向查询,则仅查询前几个(千个)元素。如果您的foreach用完了所获取的数据,则会查询更多数据。

因此,您已经有一个IQueryable<T>,因此您有一个Expression,一个Provider和一个ElementType。在执行之前,您需要相同的Expression / ElementType to be executed by a different Provider . You even want to change the Expression`。

因此,您需要能够创建实现IQueryable<T>的对象,并且希望能够设置ExpressionElementTypeProvider

class MyQueryable<T> : IQueryable<T>
{
     public type ElementType {get; set;}
     public Expression Expression {get; set;}
     public Provider Provider {get; set;}
} 

IQueryable<T> queryOnDbContextA= dbCotextA ...
IQueryable<T> setInDbContextB = dbContextB.Set<T>();

IQueryable<T> queryOnDbContextB = new MyQueryable<T>()
{
     ElementType = queryOnDbContextA.ElementType,
     Expression = queryOnDbContextB.Expression,
     Provider = setInDbContextB.Provider,
}

如果需要,您可以在执行其他查询之前对其进行调整:

var getPageOnContextB = queryOnDbContextB
    .Skip(...)
    .Take(...);

两个查询仍未执行。执行它们:

var countA = await queryOnContextA.CountAsync();
var fetchedPageContextB = await getPageOnContextB.ToListAsync();