使用EF Core Include连接ExpressionVisitor

时间:2018-08-28 11:05:24

标签: c# entity-framework-core

我有一个ExpressionVisitor,我将其添加到EF Core的IQueryable<T>中。除了Include方法之外,其他所有方法都工作正常。可能是因为它们将您的IQueryable<T>.Provider强制为EntityQueryProvider

每当我尝试现在包括它时,都会导致多个查询,进而导致错误“在上一个操作完成之前在此上下文上启动了第二个操作。不保证任何实例成员都是线程安全的。”

如何连接ExpressionVisitor,使其仍然可以与EF Core的包含功能一起使用?

我的问题类似于this one,除了EF Core而不是EF。

我通过在DbSet上调用ExpressionVisitor来连接它:

        return new Translator<TEntity>(
            _dbSet
                .AsNoTracking());

这是我的Translator班:

public class Translator<T> : IOrderedQueryable<T>
{
    private readonly Expression _expression;
    private readonly TranslatorProvider<T> _provider;

    public Translator(IQueryable source)
    {
        _expression = Expression.Constant(this);
        _provider = new TranslatorProvider<T>(source);
    }

    public Translator(IQueryable source, Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        _expression = expression;
        _provider = new TranslatorProvider<T>(source);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return ((IEnumerable<T>)_provider.ExecuteEnumerable(_expression)).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _provider.ExecuteEnumerable(_expression).GetEnumerator();
    }

    public Type ElementType => typeof(T);

    public Expression Expression => _expression;

    public IQueryProvider Provider => _provider;
}

这是我的TranslatorProvider<T>类(我删除了不相关的Visit方法来缩短帖子):

public class TranslatorProvider<T> : ExpressionVisitor, IQueryProvider
{
    private readonly IQueryable _source;

    public TranslatorProvider(IQueryable source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }

        _source = source;
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        return new Translator<TElement>(_source, expression);
    }

    public IQueryable CreateQuery(Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        var elementType = expression.Type.GetGenericArguments().First();
        var result = (IQueryable) Activator.CreateInstance(typeof(Translator<>).MakeGenericType(elementType),
            _source, expression);
        return result;
    }

    public TResult Execute<TResult>(Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        var result = (this as IQueryProvider).Execute(expression);
        return (TResult) result;
    }

    public object Execute(Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        var translated = Visit(expression);
        return _source.Provider.Execute(translated);
    }

    internal IEnumerable ExecuteEnumerable(Expression expression)
    {
        if (expression == null)
        {
            throw new ArgumentNullException(nameof(expression));
        }

        var translated = Visit(expression);
        return _source.Provider.CreateQuery(translated);
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        if (node.Type == typeof(Translator<T>))
        {
            return _source.Expression;
        }
        else
        {
            return base.VisitConstant(node);
        }
    }
}

1 个答案:

答案 0 :(得分:2)

显然,自定义查询提供程序不适合当前的EF Core可查询管道,因为几种方法(IncludeAsNoTracking等)要求提供程序必须为EntityQueryProvider

在撰写本文时(EF Core 2.1.2),查询翻译过程涉及多种服务-IAsyncQueryProviderIQueryCompilerIQueryModelGenerator等。它们都是可替换的,但是我看到的最容易拦截的地方是IQueryModelGenerator服务-ParseQuery方法。

因此,不用考虑自定义IQueryable / IQueryProvider的实现,请使用以下类,并将表达式访问者插入Preprocess方法内:

using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Remotion.Linq;
using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation;

class CustomQueryModelGenerator : QueryModelGenerator
{
    public CustomQueryModelGenerator(INodeTypeProviderFactory nodeTypeProviderFactory, IEvaluatableExpressionFilter evaluatableExpressionFilter, ICurrentDbContext currentDbContext)
        : base(nodeTypeProviderFactory, evaluatableExpressionFilter, currentDbContext)
    { }

    public override QueryModel ParseQuery(Expression query) => base.ParseQuery(Preprocess(query));

    private Expression Preprocess(Expression query)
    {
        // return new YourExpressionVisitor().Visit(query);               
        return query;
    }
}

并在派生上下文OnConfiguring中替换相应的EF Core服务覆盖:

optionsBuilder.ReplaceService<IQueryModelGenerator, CustomQueryModelGenerator>();

缺点是它使用的是EF Core“内部”内容,因此您应继续监视将来的更新中的更改。