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

Tri*_*nko 5 c# async-await dbcontext entity-framework-6

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

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

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

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

Iva*_*oev 5

没有标准的方法可以做到这一点。问题在于 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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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


小智 4

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

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

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

IQueryable<T> q1 = MyQuery(dbContext1);
IQueryable<T> q2 = MyQuery(dbContext2);
Run Code Online (Sandbox Code Playgroud)

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