使用EF Core Include连接ExpressionVisitor [英] Wire up ExpressionVisitor with EF Core Include

查看:102
本文介绍了使用EF Core Include连接ExpressionVisitor的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

如何连接 ExpressionVisitor ,使其仍可与EF Core的Include功能配合使用?

我的问题类似于解决方案

更新(EF Core 3.x):

内部查询管道基础结构已更改.新的查询表达式预处理扩展点为处理方法.插入它需要替换IQueryTranslationPreprocessorFactory .例如

 使用System.Linq.Expressions;命名空间Microsoft.EntityFrameworkCore.Query{公共类CustomQueryTranslationPreprocessor:RelationalQueryTranslationPreprocessor{公共CustomQueryTranslationPreprocessor(QueryTranslationPreprocessorDependencies依赖关系,RelationalQueryTranslationPreprocessorDependencies关系依赖关系,QueryCompilationContext queryCompilationContext):基础(依赖项,relationalDependencies,queryCompilationContext){}公共重写Expression Process(Expression query)=>base.Process(Preprocess(query));私有表达式预处理(表达式查询){//query = new YourExpressionVisitor().Visit(query);返回查询;}}公共类CustomQueryTranslationPreprocessorFactory:IQueryTranslationPreprocessorFactory{公共CustomQueryTranslationPreprocessorFactory(QueryTranslationPreprocessorDependencies依赖关系,RelationalQueryTranslationPreprocessorDependencies RelationalDependencies){依赖关系=依赖关系;RelationalDependencies = RelationalDependencies;}受保护的QueryTranslationPreprocessorDependencies依赖关系{}受保护的RelationalQueryTranslationPreprocessorDependencies RelationalDependencies;公共QueryTranslationPreprocessor创建(QueryCompilationContext queryCompilationContext)=>新的CustomQueryTranslationPreprocessor(Dependencies,RelationalDependencies,queryCompilationContext);}} 

  optionsBuilder.ReplaceService< IQueryTranslationPreprocessorFactory,CustomQueryTranslationPreprocessorFactory>(); 

原始:

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

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

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

 使用Microsoft.EntityFrameworkCore.Internal;使用Microsoft.EntityFrameworkCore.Query.Internal;使用Remotion.Linq;使用Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation;类CustomQueryModelGenerator:QueryModelGenerator{公共CustomQueryModelGenerator(INodeTypeProviderFactory nodeTypeProviderFactory,IEvaluatableExpressionFilter evaluatableExpressionFilter,ICurrentDbContext currentDbContext):base(nodeTypeProviderFactory,evaluatableExpressionFilter,currentDbContext){}公共重写QueryModel ParseQuery(Expression query)=>base.ParseQuery(Preprocess(query));私有表达式预处理(表达式查询){//返回新的YourExpressionVisitor().Visit(query);返回查询;}} 

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

  optionsBuilder.ReplaceService< IQueryModelGenerator,CustomQueryModelGenerator>(); 

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

I have an ExpressionVisitor which I add to EF Core's IQueryable<T>. Everything works fine except the Include methods. Probably because they enforce your IQueryable<T>.Provider to be an EntityQueryProvider.

Whenever I try to Include now it results in multiple queries which in turn results in the error "A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.".

How can I wire up my ExpressionVisitor so it still works with EF Core's Include functionality?

My issue is similar to this one except for EF Core instead of EF.

I hook up my ExpressionVisitor by calling it on the DbSet:

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

This is my Translator class:

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;
}

And this is my TranslatorProvider<T> class (I've taken out the non-relevant Visit methods to shorten the post):

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);
        }
    }
}

解决方案

Update (EF Core 3.x):

The internal query pipeline infrastructure has changed. The new query expression preprocessing extension point is QueryTranslationPreprocessor class - Process method. Plugging it in requires replacing the IQueryTranslationPreprocessorFactory. e.g.

using System.Linq.Expressions;

namespace Microsoft.EntityFrameworkCore.Query
{
    public class CustomQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
    {
        public CustomQueryTranslationPreprocessor(QueryTranslationPreprocessorDependencies dependencies, RelationalQueryTranslationPreprocessorDependencies relationalDependencies, QueryCompilationContext queryCompilationContext)
            : base(dependencies, relationalDependencies, queryCompilationContext) { }
        public override Expression Process(Expression query) => base.Process(Preprocess(query));
        private Expression Preprocess(Expression query)
        {
            // query = new YourExpressionVisitor().Visit(query);               
            return query;
        }
    }

    public class CustomQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
    {
        public CustomQueryTranslationPreprocessorFactory(QueryTranslationPreprocessorDependencies dependencies, RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
        {
            Dependencies = dependencies;
            RelationalDependencies = relationalDependencies;
        }
        protected QueryTranslationPreprocessorDependencies Dependencies { get; }
        protected RelationalQueryTranslationPreprocessorDependencies RelationalDependencies;
        public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
            => new CustomQueryTranslationPreprocessor(Dependencies, RelationalDependencies, queryCompilationContext);
    }
}

and

optionsBuilder.ReplaceService<IQueryTranslationPreprocessorFactory, CustomQueryTranslationPreprocessorFactory>();

Original:

Apparently custom query providers don't fit in the current EF Core queryable pipeline, since several methods (Include, AsNoTracking etc.) require provider to be EntityQueryProvider.

At the time of writing (EF Core 2.1.2), the query translation process involves several services - IAsyncQueryProvider, IQueryCompiler, IQueryModelGenerator and more. All they are replaceable, but the easiest place for interception I see is the IQueryModelGenerator service - ParseQuery method.

So, forget about custom IQueryable / IQueryProvider implementation, use the following class and plug your expression visitor inside Preprocess method:

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;
    }
}

and replace the corresponding EF Core service inside your derived context OnConfiguring override:

optionsBuilder.ReplaceService<IQueryModelGenerator, CustomQueryModelGenerator>();

The drawback is that this is using EF Core "internal" stuff, so you should keep monitoring for changes in the future updates.

这篇关于使用EF Core Include连接ExpressionVisitor的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆